Created
August 3, 2020 14:53
-
-
Save ttennebkram/357543881836afdd654c40cb4109bca8 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Problem Method | |
# --------------- | |
# Final gist, w code fixes | |
# License: BSD | |
# | |
# | |
# On advice of one Stack Overflow user I still keep going | |
# to also see when I have zero columns AS WELL. | |
# (In this case I have header row/col, so I can track columns with zero records. | |
# This change did change the behavior, but not convinvence that it's correct. | |
# | |
# The Problem to Solve: In English | |
# ================================= | |
# I'm following this example to the letter, exact with | |
# a simple 3-piece Pentominoes puzzle. | |
# My bible for this project: | |
# https://en.wikipedia.org/wiki/Knuth%27s_Algorithm_X#Example | |
# Asking folks on Stack overflow here: | |
# https://stackoverflow.com/help/minimal-reproducible-example | |
# Note about Implementation | |
# --------------------------- | |
# This third implementation I've done of the Wikipedia Dancing Links Demo, | |
# this time using NumPy arrays instead of linked lists. | |
# So I can "cover" rows and columns with simple array delete commands. | |
# To backtrack and restore state, where you "uncover" previously covered rows & cols, | |
# instead you just keep a fresh copy of the np array as a pre-move-copy to put back in place | |
# if there's a need for a backtrack. The numpy copy is VERY fast, no worries. | |
# IMPORTS | |
def ___Imports___(): pass | |
# ============================= | |
import re | |
import numpy as np | |
# Don't change these! | |
NUM_ROTAION_POSITIONS = 4 | |
NUM_REFLECTION_VALUES = 2 | |
SHOW_BAD_BOARDS = True | |
# Using this to create a very small example | |
USE_REDUCED_PENTO_PUZZLE = False # L, T, Y | |
# TODO: ^-- doesn't switch everything over | |
USE_SECONDARY_REDUCED_PUZZLE = False # P, U, V | |
# ^-- Careful with this variable, it changes a lot of things | |
# Tiny examples: | |
# * only 3 pieces vs 12 | |
# * board is only 3x5 | |
# Tiny Set 1: 3x5 | |
# l l l l t | |
# l y t t t | |
# y y y y t | |
# | |
# Tiny Set 2: 3x5 | |
# u u v v v | |
# u p p p v | |
# u u p p v | |
class Piece: | |
def clone( self ): | |
newPiece = Piece( self.name, self.width, self.height, self.bitmap[:] ) | |
# state | |
newPiece.r = self.r | |
newPiece.c = self.c | |
newPiece.rotation = self.rotation | |
newPiece.reflection = self.reflection | |
newPiece.possible_rotations = self.possible_rotations | |
newPiece.possible_reflections = self.possible_reflections | |
return newPiece | |
def _x_clone( self, src ): | |
self.name = src.name | |
self.height = src.height | |
self.width = src.width | |
# special | |
self.bitmap = src.bitmap[:] | |
# state | |
self.r = src.r | |
self.c = src.c | |
self.rotation = src.rotation | |
self.reflection = src.reflection | |
# For Piece | |
def _v0__repr__(self): | |
heading = f"Piece \"{self.name}\" is {self.height} rows by {self.width} columns.\n" | |
heading2 = f"at r,c {self.r},{self.c}, rotation={self.rotation}, reflection={self.reflection}\n" | |
buffer = '\n'.join( [ "".join( [ str(self.bitmap[y][x]) for x in range(self.width)] ) for y in range(self.height) ] ) | |
output = heading + heading2 + buffer | |
return output | |
def __repr__(self): | |
# v-- similar to, but still diff from, Move.__repr__ | |
output = f"{self.name}_{self.height},{self.width}_rr{self.rotation}{self.reflection}" | |
return output | |
# For Piece | |
def __init__( self, name, height, width, bitmap ): | |
self.name = name | |
self.height = height | |
self.width = width | |
self.bitmap = bitmap | |
# ? _self.transformedBitmap = None # added by Board, since it does transforms | |
# can track board placement coord stats | |
self.r = 0 | |
self.c = 0 | |
self.rotation = 0 | |
self.reflection = 0 | |
self.possible_rotations = 0 | |
self.possible_reflections = 0 | |
# STATIC | |
def getBlankPieceBitmap( rows, cols ): | |
# newPiece = [ [ ' ' for x in range(cols)] for y in range(rows) ] | |
newPiece = [ [ 0 for x in range(cols)] for y in range(rows) ] | |
return newPiece | |
# def rotate90CounterClockwise( self, pieceObj ): | |
# STATIC | |
def rotate90CounterClockwise( pieceObj ): | |
# Note: Intentionally swapping height and width, to support rotated piece | |
dstBitmap = Piece.getBlankPieceBitmap( pieceObj.width, pieceObj.height ) | |
srcBitmap = pieceObj.bitmap | |
for srcR in range( pieceObj.height ): | |
for srcC in range( pieceObj.width ): | |
value = srcBitmap[ srcR ][ srcC ] | |
# map to new coordinates | |
dstC = srcR | |
# In 3x3, 0->2, 1->1, 2->0 | |
complimentC = pieceObj.width - srcC - 1 | |
dstR = complimentC | |
dstBitmap[ dstR ][ dstC ] = value | |
# again, NOTICE we're making sure to swap row & col | |
newPiece = Piece( pieceObj.name, pieceObj.width, pieceObj.height, dstBitmap ) | |
newPiece.rotation = ( pieceObj.rotation + 1 ) % NUM_ROTAION_POSITIONS # 4 | |
newPiece.bitmap = dstBitmap | |
return newPiece | |
# cloning happens in rotate90 --^ | |
# def rotateAsNeeded( self, pieceObj, rotation ): | |
# STATIC | |
def rotateAsNeeded( pieceObj, rotation ): | |
outPiece = pieceObj | |
for _ in range( rotation ): # 4 directions, 0,1,2,3 | |
# outPiece = self.rotate90CounterClockwise( outPiece ) | |
outPiece = Piece.rotate90CounterClockwise( outPiece ) | |
outPiece.rotation = rotation | |
# I give up, try explicit copy here | |
outPiece.reflection = pieceObj.reflection | |
return outPiece | |
# def reflectAsNeeded( self, pieceObj, reflection ): | |
# STATIC | |
def reflectAsNeeded( pieceObj, reflection ): | |
# if even number (including 0) just return original piece | |
if reflection % NUM_REFLECTION_VALUES == 0: | |
# return pieceObj | |
# TODO: clone, so we can add reflection state | |
# ^-- actually, "no reflection" is 0 anyway | |
# but consumer might change others... | |
# Ultimately I think expectation is that you're getting a copy | |
# TODO: wasteful to keep doing... caching -> moot point... ? | |
outPiece = pieceObj.clone() | |
# will already have bitmap filled in | |
return outPiece | |
newBitmap = [] | |
for rowData in pieceObj.bitmap: | |
# reversedRowData = rowData.reverse() | |
rowData.reverse() | |
# newBitmap.append( reversedRowData ) | |
newBitmap.append(rowData) | |
outPiece = Piece( pieceObj.name, pieceObj.height, pieceObj.width, newBitmap ) | |
outPiece.reflection = reflection % NUM_REFLECTION_VALUES | |
# I give up, try explicit copy here | |
outPiece.rotation = pieceObj.rotation | |
outPiece.bitmap = newBitmap | |
return outPiece | |
# STATIC | |
# def generateAllPieceVariations( self ): | |
def generateAllPieceVariations(): | |
allPieceVariations = [] # local | |
pieceCheckSet = set() | |
# print( f"Debug: generateAllPieceVariations: pieces has {len(pieces)} entries", flush=True ) | |
for i, piece in enumerate( pieces ): # alphabetical order | |
for rotation in range( piece.possible_rotations ): | |
for reflection in range( piece.possible_reflections ): | |
transformedPieceObj = None | |
# print( f"Debug: generateAllPieceVariations: piece {piece} plus rr {rotation} {reflection}" ) | |
## if rotation == 0 and reflection == 0: | |
## # piece already has bitmap | |
## transformedPieceObj = piece.clone() | |
## else: | |
## # these do cloning | |
## transformedPieceObj = Piece.rotateAsNeeded( piece, rotation ) | |
## transformedPieceObj = Piece.reflectAsNeeded( transformedPieceObj, reflection ) | |
transformedPieceObj = Piece.rotateAsNeeded( piece, rotation ) | |
transformedPieceObj = Piece.reflectAsNeeded( transformedPieceObj, reflection ) | |
# print( f"Debug: generateAllPieceVariations: NOW {transformedPieceObj} for rr {rotation} {reflection}" ) | |
piece.transformedPiece = transformedPieceObj | |
# print( f'Debug: Transformed piece rotation = {transformedPieceObj.rotation}', flush=True ) | |
#pieceStr = repr(piece) | |
pieceStr = repr(transformedPieceObj) | |
# print( f'Debug: Checking piece "{pieceStr}"', flush=True ) | |
if pieceStr in pieceCheckSet: | |
raise ValueError( f'Already saw a "{pieceStr}" piece' ) | |
else: | |
pieceCheckSet.add( pieceStr ) | |
allPieceVariations.append( transformedPieceObj ) | |
## newMove = StackEntry( r, c, piece.name, rotation, reflection ) | |
## newMove.transformedPiece = transformedPieceObj | |
## newMove.newBitmap = self.cloneBitmap( transformedPieceObj.bitmap ) | |
## possibleMovesQueue.append( newMove ) | |
return allPieceVariations | |
F = Piece( "f", 3, 3, [ | |
[ 0, 1, 1 ], | |
[ 1, 1, 0 ], | |
[ 0, 1, 0 ], | |
] | |
) | |
F.possible_rotations = 4 | |
F.possible_reflections = 2 | |
I = Piece( "i", 5, 1, [ | |
[ 1 ], | |
[ 1 ], | |
[ 1 ], | |
[ 1 ], | |
[ 1 ], | |
] | |
) | |
I.possible_rotations = 2 | |
I.possible_reflections = 1 | |
L = Piece( "l", 4, 2, [ | |
[ 1, 0 ], | |
[ 1, 0 ], | |
[ 1, 0 ], | |
[ 1, 1 ], | |
] | |
) | |
L.possible_rotations = 4 | |
L.possible_reflections = 2 | |
N = Piece( "n", 2, 4, [ | |
[ 1, 1, 0, 0 ], | |
[ 0, 1, 1, 1 ], | |
] | |
) | |
N.possible_rotations = 4 | |
N.possible_reflections = 2 | |
P = Piece( "p", 3, 2, [ | |
[ 1, 1 ], | |
[ 1, 1 ], | |
[ 1, 0 ], | |
] | |
) | |
P.possible_rotations = 4 | |
P.possible_reflections = 2 | |
T = Piece( "t", 3, 3, [ | |
[ 1, 1, 1 ], | |
[ 0, 1, 0 ], | |
[ 0, 1, 0 ], | |
] | |
) | |
T.possible_rotations = 4 | |
T.possible_reflections = 1 | |
U = Piece( "u", 2, 3, [ | |
[ 1, 0, 1 ], | |
[ 1, 1, 1 ], | |
] | |
) | |
U.possible_rotations = 4 | |
U.possible_reflections = 1 | |
V = Piece( "v", 3, 3, [ | |
[ 1, 0, 0 ], | |
[ 1, 0, 0 ], | |
[ 1, 1, 1 ], | |
] | |
) | |
V.possible_rotations = 4 | |
V.possible_reflections = 1 | |
W = Piece( "w", 3, 3, [ | |
[ 1, 0, 0 ], | |
[ 1, 1, 0 ], | |
[ 0, 1, 1 ], | |
] | |
) | |
W.possible_rotations = 4 | |
W.possible_reflections = 1 | |
X = Piece( "x", 3, 3, [ | |
[ 0, 1, 0 ], | |
[ 1, 1, 1 ], | |
[ 0, 1, 0 ], | |
] | |
) | |
X.possible_rotations = 1 | |
X.possible_reflections = 1 | |
Y = Piece( "y", 2, 4, [ | |
[ 0, 0, 1, 0 ], | |
[ 1, 1, 1, 1 ], | |
] | |
) | |
Y.possible_rotations = 4 | |
Y.possible_reflections = 2 | |
Z = Piece( "z", 3, 3, [ | |
[ 1, 1, 0 ], | |
[ 0, 1, 0 ], | |
[ 0, 1, 1 ], | |
] | |
) | |
Z.possible_rotations = 2 | |
Z.possible_reflections = 2 | |
# Full Set | |
# =========== | |
# the 12 pieces, Alphabetical | |
# Uppercase letters are actual objects of type Piece | |
piecesAlphabetical = [ F, I, L, N, P, T, U, V, W, X, Y, Z ] | |
# Code knows not to put f in the upper left corner, for example, | |
# since it would create a one-block hole | |
piecesNames = [ 'f', 'i', 'l', 'n', 'p', 't', 'u', 'v', 'w', 'x', 'y', 'z', ] | |
piecesStr = "filnptuvwxyz" | |
piecesByName = { | |
'f': F, | |
'i': I, | |
'l': L, | |
'n': N, | |
'p': P, | |
't': T, | |
'u': U, | |
'v': V, | |
'w': W, | |
'x': X, | |
'y': Y, | |
'z': Z, | |
} | |
# Tiny Examples | |
# ----------------- | |
# input some tiny solutions | |
# | |
# Tiny Set 1: 3x5 | |
# l l l l t | |
# l y t t t | |
# y y y y t | |
# | |
# Tiny Set 2: 3x5 | |
# u u v v v | |
# u p p p v | |
# u u p p v | |
# | |
# 3x5 | |
pieces_3x5_tinySet1 = [ L, T, Y ] | |
pieces_3x5_tinySet2 = [ P, U, V ] | |
# | |
# 6x5 | |
pieces_6x5_stacked_top_bottom = [ L, P, T, U, V, Y, ] | |
# | |
# 3x10 | |
pieces_3x10_stacked_left_right = [ L, P, T, U, V, Y, ] | |
# TODO: could do other combos | |
piecesStr_tinySet1 = "lty" | |
piecesNames_tinySet1 = [ 'l', 't', 'y', ] | |
piecesAlphabeticalReduced1 = [ L, T, Y ] | |
piecesByName_Reduced1 = { | |
'l': L, | |
't': T, | |
'y': Y, | |
} | |
piecesStr_tinySet2 = "puv" | |
piecesNames_tinySet2 = [ 'p', 'u', 'v', ] | |
piecesAlphabeticalReduced2 = [ P, U, V ] | |
piecesByName_Reduced2 = { | |
'p': P, | |
'u': U, | |
'v': V, | |
} | |
pieces = None | |
piecesStr = None | |
piecesByName = None | |
# Choose which piece set | |
# pieces = piecesAlphabetical | |
if USE_REDUCED_PENTO_PUZZLE: | |
print( f"Debug: Using REDUCED logic", flush=True ) | |
if not USE_SECONDARY_REDUCED_PUZZLE: | |
print( f"Debug: Using primary reduced logic example", flush=True ) | |
pieces = piecesAlphabeticalReduced1 # L, T, Y | |
piecesStr = piecesStr_tinySet1 = "lty" | |
piecesByName = piecesByName_Reduced1 | |
# else DO use Secondary | |
else: | |
print( f"Debug: Using secondary reduced logic example", flush=True ) | |
pieces = piecesAlphabeticalReduced2 # P, U, V | |
piecesStr = piecesStr_tinySet2 # p, u, v? | |
piecesByName = piecesByName_Reduced2 | |
else: | |
print( f"Debug: Using FULL (vs. reduced) logic", flush=True ) | |
pieces = piecesAlphabetical | |
piecesStr = "filnptuvwxyz" | |
piecesByName = piecesByName | |
# Global Variables | |
def ___Globals___(): pass | |
# ============================ | |
# REDUCED Test mode | |
if USE_REDUCED_PENTO_PUZZLE: | |
MOVES_FILE = "moves-file-experimental.txt" | |
HEIGHT = 3 | |
WIDTH = 5 | |
# REDUCED/REDACTED Test Mode | |
BOARD_HEIGHT = 3 | |
BOARD_WIDTH = 5 | |
NUM_BLOCKS_PER_PIECE = 5 | |
NUMBER_BOARD_CELLS = 15 | |
# NUM_BITS = NUMBER_BOARD_CELLS + len(blocks3.pieces) | |
NUM_BITS = NUMBER_BOARD_CELLS + len(pieces) | |
print( f"Debug: globals: NUM_BITS = {NUM_BITS}" ) | |
DATA_FILE = "moves-file-experimental.txt" | |
else: | |
# Normal / Full MODE | |
MOVES_FILE = "moves-file.txt" | |
HEIGHT = 6 | |
WIDTH = 10 | |
# STANDARD MODE | |
# Warning: Not everything tested for changing these... | |
BOARD_HEIGHT = 6 | |
BOARD_WIDTH = 10 | |
# Works, and is fast! | |
## HEIGHT = 3 | |
## WIDTH = 20 | |
## BOARD_HEIGHT = 3 | |
## BOARD_WIDTH = 20 | |
NUM_BLOCKS_PER_PIECE = 5 | |
NUMBER_BOARD_CELLS = 60 | |
# NUM_BITS = NUMBER_BOARD_CELLS + len(blocks3.pieceNames) | |
# NUM_BITS = NUMBER_BOARD_CELLS + len(piecesNames) | |
NUM_BITS = NUMBER_BOARD_CELLS + len( pieces ) | |
DATA_FILE = "moves-file.txt" | |
g_debug = False | |
# g_debug = True | |
DEFAULT_STACK_HEIGHT = 100 | |
# wc -l --> 1928 moves-file.txt | |
# TODO: may not be used, need to trace through | |
_MOVES_HEIGHT = 1_928 | |
_MOVES_WIDTH = 72 | |
g_allPossibleMoves = [] | |
g_rowNames = [] | |
g_rowNamesToBitMoves = {} | |
g_bitMovesToRowNames = {} | |
# Maintains All Of Our State! | |
# --------------------------- | |
# Original NumPy board after init but before solve | |
g_npOrigArray = None | |
g_PendingSolutionPerLevel = [ None for _ in range(DEFAULT_STACK_HEIGHT) ] | |
# Raw Stats | |
g_attemptedPlacementCounter = 0 | |
g_holesCheckCounter = 0 | |
# | |
# Every valid move, including piece, position, rotation & reflection | |
# First 12 bits are for each piece name | |
# last 60 bits are for each space on the board | |
# | |
g_allPossibleMoves = [] | |
g_rowNames = [] | |
# Custom Exceptions | |
def ___Custom_Exceptions___(): pass | |
# ================================= | |
class ZeroValueColumn(Exception): pass | |
class EmptyBoard_NoColumns(Exception): pass # this is GOOD --> check for solution | |
class ZeroColumns(Exception): pass | |
class EmptyBoard_NoRows(Exception): pass # whereas this is BAD --> backtrack | |
class NoIntersectingRows(Exception): pass | |
class NoIntersectingCols(Exception): pass | |
class UnexpectedPentosBoardCollision(Exception): pass | |
class InvalidPentosBoard(Exception): pass | |
class WontFitOnBoard(Exception): pass | |
class BlockedByPreviousPiece(Exception): pass | |
class InvalidHoleSize(Exception): pass | |
# Class | |
# ===================================================== | |
# Tiny Examples | |
# ----------------- | |
# input some tiny solutions | |
# | |
# Tiny Set 1: 3x5 | |
# l l l l t | |
# l y t t t | |
# y y y y t | |
# | |
# Tiny Set 2: 3x5 | |
# u u v v v | |
# u p p p v | |
# u u p p v | |
# | |
# the normal 12 pieces, Alphabetical | |
piecesAlphabetical = [ F, I, L, N, P, T, U, V, W, X, Y, Z ] | |
# | |
# 3x5 | |
pieces_3x5_tinySet1 = [ L, T, Y ] | |
pieces_3x5_tinySet2 = [ P, U, V ] | |
# | |
# 6x5 | |
pieces_6x5_stacked_top_bottom = [ L, P, T, U, V, Y, Z ] | |
# | |
# 3x10 | |
pieces_3x10_stacked_left_right = [ L, P, T, U, V, Y, Z ] | |
# TODO: could do other combos | |
# Choose which piece set | |
pieces = piecesAlphabetical | |
# pieces = pieces_3x5_tinySet1 | |
piecesNames = [ 'f', 'i', 'l', 'n', 'p', 't', 'u', 'v', 'w', 'x', 'y', 'z', ] | |
# piecesNames = [ 'l', 't', 'y', ] | |
_piecesStr = "filnptuvwxyz" | |
_piecesByName = { | |
'f': F, | |
'i': I, | |
'l': L, | |
'n': N, | |
'p': P, | |
't': T, | |
'u': U, | |
'v': V, | |
'w': W, | |
'x': X, | |
'y': Y, | |
'z': Z, | |
} | |
# aka Node, StackEntry | |
class Move: | |
# def __init__( self, encodedString ): | |
# "static" | |
def generateMoveFromString( encodedString ): | |
if not ( m := re.match(DECODE_MOVE_NAME_REGEX, encodedString ) ): | |
raise ValueError( f'Input string "{encodedString}" did not match Regex "{DECODE_MOVE_NAME_REGEX}"' ) | |
else: | |
pieceName = m.group(1) | |
r = int( m.group(2) ) | |
c = int( m.group(3) ) | |
rotation = int( m.group(4) ) | |
reflection = int( m.group(5) ) | |
return Move( pieceName, r, c, rotation, reflection ) | |
# self.piece = g_piecesByName[ pieceName ] | |
# self.transformedPiece = None | |
# self.prevBoardBitmap = None | |
# self.newBoardBitmap = None | |
# self.possibleMovesPointer = -1 | |
# def __init__( self, r, c, pieceName, rotation, reflection ): | |
# For Move | |
def __init__( self, pieceName, r, c, rotation, reflection ): | |
self.r, self.c = r, c | |
self.pieceName = pieceName | |
def prepareBitmaps(): | |
pass | |
# "color" is just a digit 1 to 9 or string | |
def findFirstPixel( boardBitmap, targetColor ): | |
for r in range( len(boardBitmap) ): | |
for c in range( len(boardBitmap[0]) ): | |
value = boardBitmap[ r ][ c ] | |
if value == targetColor: | |
return ( r, c ) | |
return ( -1, -1 ) | |
# | |
# see also https://www.geeksforgeeks.org/flood-fill-algorithm-implement-fill-paint/ | |
# Does check for invalid r,c coordinates | |
# | |
# Used to spot holes in the board that are not a multiple of 5 spaces | |
def floodFill( boardBitmap, r, c, oldColor, newColor ): | |
if r < 0 or r >= len(boardBitmap): | |
return | |
if c < 0 or c >= len(boardBitmap[0]): | |
return | |
currColor = boardBitmap[ r ][ c ] | |
if currColor != oldColor: | |
return | |
else: | |
# The Target is a match, so change color | |
boardBitmap[ r ][ c ] = newColor | |
# Now do the recursion for the 4 Edge neighbors | |
# North | |
north_row = r - 1 | |
north_col = c | |
floodFill( boardBitmap, north_row, north_col, oldColor, newColor ) | |
# South | |
south_row = r + 1 | |
south_col = c | |
floodFill( boardBitmap, south_row, south_col, oldColor, newColor ) | |
# East | |
east_row = r | |
east_col = c + 1 | |
floodFill( boardBitmap, east_row, east_col, oldColor, newColor ) | |
# West | |
west_row = r | |
west_col = c - 1 | |
floodFill( boardBitmap, west_row, west_col, oldColor, newColor ) | |
# No need to "return" the board | |
return | |
# Used to spot holes in the board that are not a multiple of 5 spaces | |
def histogramOfNumericColors( boardBitmap ): | |
histogram = {} | |
for r in range( len(boardBitmap) ): | |
for c in range( len(boardBitmap[0]) ): | |
currValue = boardBitmap[ r ][ c ] | |
if re.match( "[0-9]+", currValue ): | |
prevCount = histogram[ currValue ] if currValue in histogram.keys() else 0 | |
newCount = prevCount + 1 | |
histogram[ currValue ] = newCount | |
return histogram | |
# Used to spot holes in the board that are not a multiple of 5 spaces | |
def checkHoleSizes( boardBitmap ): | |
global g_holesCheckCounter | |
g_holesCheckCounter += 1 | |
# boardBitmapClone = boardBitmap[:] | |
boardBitmapClone = cloneBitmap( boardBitmap ) | |
colorNumeric = 1 | |
# Label and fill all empty-space blobs | |
while True: | |
spaceR, spaceC = findFirstPixel( boardBitmapClone, ' ' ) | |
if spaceR < 0 or spaceC < 0: | |
break | |
colorStr = str( colorNumeric ) | |
floodFill( boardBitmapClone, spaceR, spaceC, ' ', colorStr ) | |
colorNumeric += 1 | |
# Now check all hole sizes | |
histogram = histogramOfNumericColors( boardBitmapClone ) | |
# print( "Trying:" ) | |
# printBoard( boardBitmapClone ) | |
for k,v in histogram.items(): | |
if not v % NUM_BLOCKS_PER_PIECE == 0: | |
# print( "... and it Failed", flush=True ) | |
raise InvalidHoleSize( f"Hole '{k}' size ({v}) not evenly divisible by {NUM_BLOCKS_PER_PIECE}" ) | |
# either throws exception or returns board bitmap | |
# print( "... and it WORKED!", flush=True ) | |
return boardBitmapClone | |
# Lowest Level of Board/Piece logic | |
# X, No: Modifies the board in place, so caller may wish to clone | |
# Returns many types of Exceptions | |
# Note: "name" of piece is the same string as we use to "color" the board | |
# Note 2: Can throw a BUNCH of Exceptions, see also Custom Exceptions | |
def placePieceOnBoard( bitmap, pieceObj, r, c, rotation, reflection ): | |
global g_attemptedPlacementCounter | |
g_attemptedPlacementCounter += 1 | |
# newBoard = clone() | |
# newBoard = board.clone() | |
# newBoardBitmap = _boardBitmap[:] | |
newBoardBitmap = cloneBitmap( bitmap ) | |
# Error: pieceObj.bitmap has extra None rows | |
for pieceR, piece_row_data in enumerate( pieceObj.bitmap ): | |
for pieceC, piece_value in enumerate( piece_row_data ): | |
# Piece bitmaps are numeric, Board bitmaps are strings/chars | |
#if piece_value != ' ': | |
if piece_value > 0: | |
# TODO: these will include offsets | |
boardR = pieceR + r | |
boardC = pieceC + c | |
# bounds checking | |
if boardR < 0: | |
raise WontFitOnBoard( f"Board Row offset less than 0 ({boardR})" ) | |
# if boardR >= board.height: | |
if boardR >= len(newBoardBitmap): | |
# raise WontFitOnBoard( f"Board Row OFFSET too large, got {boardR} but max is only height-1 = {board.height-1}" ) | |
raise WontFitOnBoard( f"Board Row OFFSET too large, got {boardR} but max is only height-1 = {len(newBoardBitmap)-1}" ) | |
if boardC < 0: | |
raise WontFitOnBoard( f"Board Column offset less than 0 ({boardC})" ) | |
# if boardC >= board.width: | |
if boardC >= len( newBoardBitmap[0] ): | |
# raise WontFitOnBoard( f"Board Column OFFSET too large, got {boardC} but max is only width-1 = {board.width-1}" ) | |
raise WontFitOnBoard( f"Board Column OFFSET too large, got {boardC} but max is only width-1 = {len(newBoardBitmap[0])-1}" ) | |
currPieceName = newBoardBitmap[ boardR ][ boardC ] # remember single-char "name" is also the "color" on the board | |
if currPieceName != ' ': | |
raise BlockedByPreviousPiece( f"Position r,c = {boardR},{boardC} already contains \"{currPieceName}\" when trying to place \"{pieceObj.name}\" pixel." ) | |
newBoardBitmap[ boardR ][ boardC ] = pieceObj.name | |
# else piece value is a zero / empty space, so OK | |
return newBoardBitmap | |
## # for Board | |
## def __init__( self ): | |
## self.height = MOVES_HEIGHT | |
## self.width = MOVES_WIDTH | |
## def __init__( self, height = HEIGHT, width = WIDTH ): | |
## if height * width != NUMBER_BOARD_CELLS: # 60 | |
## raise ValueError( f"{height} x {width} = {height*width} is incorrect area, must be {NUMBER_BOARD_CELLS}" ) | |
## self.height = height | |
## self.width = width | |
## self.height = height | |
## self.width = width | |
## self.bitmap = self.getBlankBoard_asBitmap() | |
# v1 is the more elegant code | |
# v2 was very manual wrt to literal arrays | |
# Upgraded code goes back to v1 and adds in piece variations | |
def generateAllPossibleMoves_v2(): | |
global g_allPossibleMoves # shouldn't need this since it's a reference | |
global g_rowNames | |
# allPieceVariations = blocks1.Piece.generateAllPieceVariations() | |
allPieceVariations = Piece.generateAllPieceVariations() | |
print( f"Debug: generateAllPossibleMoves_v2: New: len(v1+.generateAllPieceVariations()) = {len(allPieceVariations):,}" ) | |
#print( f"Debug: generateAllPossibleMoves_v2: Exp: len(v3+.generateAllPieceVariations()) = {len(expPieceVariations):,}" ) | |
rowNamesCheck = set() | |
# Foreach Piece Variation | |
for i,p in enumerate( allPieceVariations ): | |
# for i,p in enumerate( expPieceVariations ): | |
# print( f"Debug: piece variation #{i+1}: for piece {p.name}") | |
for r in range( BOARD_HEIGHT ): | |
for c in range( BOARD_WIDTH ): | |
# ALWAYS start with new board | |
# newBoard = Board() | |
newBitmap = getBlankBoard_asBitmap() | |
name = p.name | |
nameBits = pieceNameToBinaryNumber( name ) | |
# rowName will encode the piece/move information | |
rowName = p.name + '_' + str(r) + ',' + str(c) + "_rr" + str(p.rotation) + str(p.reflection) | |
if rowName in rowNamesCheck: | |
raise ValueError( f'Already have "{rowName}"' ) | |
else: | |
rowNamesCheck.add( rowName ) | |
try: | |
# newBitmap = newBoard.placePieceOnBoard( newBitmap, p, r, c, p.rotation, p.reflection ) | |
newBitmap = placePieceOnBoard( newBitmap, p, r, c, p.rotation, p.reflection ) | |
# Often won't get here due to very comm exceptions, eg: won't fit | |
# TODO: also check hole sizes | |
bitmap = newBitmap | |
# Check hole sizes | |
_ = checkHoleSizes( bitmap ) | |
boardBits = boardBitmapToBinaryNumber( bitmap ) | |
# Success, keep this one | |
finalBitmask = (nameBits << NUMBER_BOARD_CELLS) | boardBits | |
g_allPossibleMoves.append( finalBitmask ) | |
g_rowNames.append( rowName ) | |
# ^--newly deduped | |
# Stack Overflow | |
# g_rowNames.append(str(hash(str(finalBitmask)))) | |
# catch (WontFitOnBoard, BlockedByPreviousPiece) as e1: | |
except (WontFitOnBoard, BlockedByPreviousPiece) as e1: | |
# Don't even report it | |
# No need to save state, new board each time | |
# bitmap = savedBitmap | |
continue | |
except InvalidHoleSize as e2: | |
# bitmap = savedBitmap | |
continue | |
# end for col | |
# Input: grid of characters | |
# Output: long int with bits set | |
def boardBitmapToBinaryNumber( bitmap ): | |
counter = 0 | |
answer = 0x0 | |
for r in range( BOARD_HEIGHT ): | |
for c in range( BOARD_WIDTH ): | |
counter += 1 | |
currValue = bitmap[ r ][ c ] | |
bit = 1 if currValue != ' ' else 0 | |
# offset = r * BOARD_WIDTH + c | |
offset = (BOARD_HEIGHT - r - 1) * BOARD_WIDTH + (BOARD_WIDTH - c - 1) | |
# print( f"Debug: r,c {r},{c} offset={offset} counter={counter}", flush=True ) | |
bit = bit << offset | |
answer |= bit | |
return answer | |
# Gives back a shifted bitmask of 12 bits | |
# in the proper left-to-right order | |
def pieceNameToBinaryNumber( name ): | |
# nameOffset = blocks3.piecesNames.index( name ) | |
nameOffset = piecesNames.index( name ) | |
# 12 -> 0, 0 -> 12 | |
# complimentNumber = len(blocks3.piecesNames) - nameOffset - 1 | |
complimentNumber = len(piecesNames) - nameOffset - 1 | |
answer = 1 << complimentNumber | |
return answer | |
def ___BOARD_Init_Blanks_Clones_and_Print___(): pass | |
# ==================================================== | |
# Use List Comprehensions | |
# Pentos Board | |
def printBoard_toString( boardBitmap ): | |
buffer = '\n'.join( [ " ".join( [ boardBitmap[y][x] for x in range(len(boardBitmap[0]))] ) for y in range(len(boardBitmap)) ] ) | |
return buffer | |
# Pentos Board | |
def printBoard( boardBitmap ): | |
buffer = printBoard_toString( boardBitmap ) | |
print( buffer, flush=True ) | |
# Should work with either board or piece bitmap | |
# even though board has char-based entries and pieces have numeric bitmaps | |
# we don't mess with that level, just copy it over | |
# def cloneBoardBitmap( self, inBitmap ): | |
def cloneBitmap( inBitmap ): | |
#print( f"Debug: cloneBitmap: inBitmap IS_A {type(inBitmap)}", flush=True ) | |
newBoard = [ [ inBitmap[y][x] for x in range(len(inBitmap[0])) ] for y in range(len(inBitmap)) ] | |
return newBoard | |
# getEmptyBoard sic | |
def getBlankBoard_asBitmap(): | |
newBoard = [ [ ' ' for x in range(BOARD_WIDTH)] for y in range(BOARD_HEIGHT) ] | |
return newBoard | |
### def __repr__(self): | |
### heading = f"Board is {BOARD_HEIGHT} rows by {BOARD_WIDTH} columns.\n" | |
### buffer = '\n'.join( [ " ".join( [ bitmap[y][x] for x in range(self.width)] ) for y in range(BOARD_HEIGHT) ] ) | |
### output = heading + buffer | |
### return output | |
def writeMovesToFile( fname=MOVES_FILE ): | |
print( f'Writing all possible moves ({len(g_allPossibleMoves):,}) to "{fname}"', flush=True ) | |
with open( fname, "w" ) as fout: | |
for i,move in enumerate( g_allPossibleMoves ): | |
rowName = g_rowNames[ i ] # don't feel like using zip... | |
print( f"{rowName}|{move:0{NUM_BITS}b}", file=fout ) | |
def ___Printing___(): pass | |
# ------------------------------- | |
def printBoard_np( board=g_npOrigArray ): | |
print( board ) | |
# Human readable move names: f_2,3_rr31 | |
# raises UnexpectedPentosBoardCollision | |
# unless you tell it to shut up | |
# rowNumber vs. rowOffset: the data is sortof 1-based due to headers | |
# NumPy version | |
def plotMoveToBoard_np( pentoBoard, rowNumber, ignoreExceptions=False ): | |
fancyRowHeaderName = g_rowNames[ rowNumber-1 ] | |
moveName = fancyRowHeaderName | |
# v-- Error: IndexError: invalid index to scalar variable. | |
# and if it's a string, it would still work, so what is it then? | |
# v-- "<class 'numpy.int16'>" | |
#print( f"Debug: moveName IS_A {type(moveName)}", flush=True ) | |
pieceName = moveName[ 0 ] | |
fullBitmap = g_rowNamesToBitMoves[ moveName ] | |
# boardBitmap = fullBitmap[ 12 : ] | |
# boardBitmap = fullBitmap[ 3 : ] | |
boardBitmap = fullBitmap[ len(pieces) : ] | |
for fullOffset, value in enumerate( boardBitmap ): | |
if value == "1": | |
rowOffset = fullOffset // BOARD_WIDTH | |
colOffset = fullOffset % BOARD_WIDTH | |
prevValue = pentoBoard[ rowOffset ][ colOffset ] | |
pentoBoard[ rowOffset ][ colOffset ] = pieceName | |
if prevValue != ' ': | |
# print( f"COLLISION: was '{prevValue}' now '{pieceName}'", flush=True ) | |
if not ignoreExceptions: | |
raise UnexpectedPentosBoardCollision( f"At {rowOffset},{colOffset}: There was already a '{prevValue}' present when trying to place '{pieceName}'" ) | |
# Human readable move names: f_2,3_rr31 | |
# raises UnexpectedPentosBoardCollision | |
def _plotMoveToBoard( pentoBoard, rowHeaderNode, ignoreExceptions=False ): | |
moveName = rowHeaderNode.name | |
pieceName = moveName[ : 1 ] | |
fullBitmap = g_rowNamesToBitMoves[ moveName ] | |
boardBitmap = fullBitmap[ 12 : ] | |
for fullOffset, value in enumerate( boardBitmap ): | |
if value == "1": | |
rowOffset = fullOffset // BOARD_WIDTH | |
colOffset = fullOffset % BOARD_WIDTH | |
prevValue = pentoBoard[ rowOffset ][ colOffset ] | |
pentoBoard[ rowOffset ][ colOffset ] = pieceName | |
if prevValue != ' ': | |
# print( f"COLLISION: was '{prevValue}' now '{pieceName}'", flush=True ) | |
if not ignoreExceptions: | |
raise UnexpectedPentosBoardCollision( f"At {rowOffset},{colOffset}: There was already a '{prevValue}' present when trying to place '{pieceName}'" ) | |
# raises some InvalidPentosBoard | |
# Note: Most errors are already tripped over when trying to plot to a non-empty spot, | |
# so this method might not trigger. | |
# Conversely, although it checks the size of a piece (number of letters), | |
# it doesn't check their arrangement, so a few weird edges might slip by, | |
# but again even those would likely have already hit a plot collision as well. | |
def checkBoard( pentoBoard ): | |
histogramOfBoard = {} | |
for r in range( len(pentoBoard) ): | |
for c in range( len(pentoBoard[0]) ): | |
letter = pentoBoard[ r ][ c ] | |
if letter == ' ': | |
raise InvalidPentosBoard( f"Blank space found in what should be a fully-populated board at {r},{c}" ) | |
prevCount = histogramOfBoard[ letter ] if letter in histogramOfBoard else 0 | |
newCount = prevCount + 1 | |
histogramOfBoard[ letter ] = newCount | |
for letter,count in histogramOfBoard.items(): | |
if count % 5 != 0: | |
raise InvalidPentosBoard( f"Invalid character count '{letter}' = {count}, not evenly divisible by 5" ) | |
# otherwide we're all set | |
# allows passthrough of UnexpectedPentosBoardCollision and InvalidPentosBoard to caller | |
def plotSolutionToBoard( ignoreExceptions=False ): | |
pentoBoard = getBlankPentominoesBoard( BOARD_HEIGHT, BOARD_WIDTH ) | |
#for i,s in enumerate( g_PendingSolutionPerLevel ): | |
for i,moveRowNumber in enumerate( g_PendingSolutionPerLevel ): | |
if moveRowNumber is None: | |
break | |
# self.plotMoveToBoard( pentoBoard, move, ignoreExceptions=ignoreExceptions ) | |
plotMoveToBoard_np( pentoBoard, moveRowNumber, ignoreExceptions=ignoreExceptions ) | |
if not ignoreExceptions: | |
checkBoard( pentoBoard ) | |
buffer = '\n'.join( [ " ".join( [ pentoBoard[y][x] for x in range(len(pentoBoard[0]))] ) for y in range(len(pentoBoard)) ] ) | |
return buffer | |
# aka def printPendingSolution | |
# aka def printTentativeSolution | |
def printCandidateSolution(): | |
print( "Potential Solution:" ) | |
for i,moveOffset in enumerate( g_PendingSolutionPerLevel ): | |
if moveOffset is None: | |
break | |
# fancyName = g_rowNames[ moveOffset ] | |
fancyName = g_rowNames[ moveOffset-1 ] | |
print( f"\t{i:2}: {moveOffset} ({fancyName})" ) | |
# print( "For reference, here's the original board:" ) | |
# printBoard_v2( g_rootNodeDeepCopy ) | |
try: | |
buffer = plotSolutionToBoard() | |
print( "Valid Solution:" ) | |
print( 'v' * (BOARD_WIDTH * 2 - 1) ) | |
print( buffer ) | |
print( '^' * (BOARD_WIDTH * 2 - 1) ) | |
except (UnexpectedPentosBoardCollision, InvalidPentosBoard) as err: | |
print( "INVALID SOLUTION: Problem when plotting Pentos Board:", err ) | |
if SHOW_BAD_BOARDS: | |
buffer = plotSolutionToBoard( ignoreExceptions=True ) | |
print( "INVALID Solution:" ) | |
print( 'v' * (BOARD_WIDTH * 2 - 1) ) | |
print( buffer ) | |
print( '^' * (BOARD_WIDTH * 2 - 1) ) | |
def ___Mid_Level_Logic___(self): pass | |
# ======================================== | |
# updated to handle col/row numeric headers | |
# though it doesn't directly handle them | |
# gets called first (before getIntersectingColsFor...) | |
def findIntersectingRowsForColByColOffset_np( npArray, colOffset ): | |
debug = False | |
fullColData = npArray[ :, colOffset ] | |
colHeader = fullColData[ 0 ] | |
colData = fullColData[ 1 : ] | |
outRows = [] | |
# We speak in actual offsets for the np array | |
# when passing between functions, so that header is INCLUDED | |
# but below, remember that r is off by one | |
for r, value in enumerate(colData): | |
if value: | |
if debug: print( f"Debug: findIntersectingRowsForColByColOffset_np: if value: BOUNDS checking: npArray has {len(npArray)} rows", flush=True ) | |
# outRows.append( r ) # ? or not... TODO: | |
outRows.append( r+1 ) # compensate for header | |
if len(outRows) < 1: | |
raise NoIntersectingRows( f"Debug: findIntRowsForCol: raw colOffset={colOffset}, colHeader={colHeader}; colData={colData}" ) | |
return outRows | |
# updated to handle col/row numeric headers | |
# gets called second | |
def findIntersectingColsForRowByRowOffset_np( npArray, rowOffset ): | |
debug = False | |
if debug: print( f"Debug: findIntersectingColsForRowByRowOffset_np: Top: BOUNDS checking: npArray has {len(npArray)} rows", flush=True ) | |
# Grab the full row | |
fullRowData = npArray[ rowOffset ] | |
rowHeader = fullRowData[ 0 ] | |
rowData = fullRowData[ 1 : ] | |
outCols = [] | |
for c, value in enumerate(rowData): | |
if value: | |
if debug: print( f"Debug: findIntersectingColsForRowByRowOffset_np: if value: BOUNDS checking: npArray has {len(npArray[0])} cols, c = {c}", flush=True ) | |
#outCols.append( c ) # TODO: revisit | |
outCols.append( c+1 ) | |
if len(outCols) < 1: | |
raise NoIntersectingCols( f"Debug: findIntColsForRow: raw rowOffset={rowOffset}, rowHeader={rowHeader}; rowData={rowData}" ) | |
return outCols | |
def getMinColumnCountAndColumnOffset_np( npArray ): | |
minCount = -1 | |
firstColWithMin = -1 | |
# foreach column (except header) | |
height = npArray.shape[0] | |
width = npArray.shape[1] | |
# Check size, reminder that we have a header row and column | |
if width <= 1: | |
raise EmptyBoard_NoColumns( f"Empty Matrix (Columns); now {height} rows high x {width} cols wide, INCLUDING Headers; now data columns --> check for possible solution!" ) | |
if height <= 1: | |
raise EmptyBoard_NoRows( f"Empty Matrix (Rows); now {height} rows high x {width} cols wide, INCLUDING Headers; invalid solution, backtrack" ) | |
for c in range( 1, npArray.shape[1] ): | |
colData = npArray[ :, c ] | |
colData = colData[ 1 : ] | |
currColCount = sum( colData ) | |
if minCount < 0 or currColCount < minCount: | |
minCount = currColCount | |
firstColWithMin = c | |
if minCount < 1: | |
raise ZeroValueColumn( f"Found column w sum/count of 0, this means we're not on a path to a valid solution, column={firstColWithMin}; Backtrack?" ) | |
return minCount, firstColWithMin | |
def ___Counting___(): pass | |
# ============================== | |
def _updateColumnCounts(): | |
colHeaderNode = g_rootNode.right | |
# For every Column | |
while True: | |
counter = 0 | |
currNode = colHeaderNode.down | |
# For each data node in this column | |
while True: | |
if not currNode.isHeaderNode: | |
counter += 1 | |
currNode = currNode.down | |
else: | |
break | |
colHeaderNode.currNodesInColumnCount = counter | |
colHeaderNode = colHeaderNode.right | |
if colHeaderNode.isRootNode: | |
break | |
return counter | |
# For any cell we can always go to the header | |
def _countColumns(): | |
currColHeader = g_rootNode.right | |
# if header.next == header ... then empty | |
if currColHeader.isRootNode: | |
return 0 | |
count = 0 | |
seenColumnNames = set() | |
seenColumnNames.add( g_rootNode.name ) | |
while True: | |
if currColHeader.name in seenColumnNames: | |
break | |
else: | |
count += 1 | |
seenColumnNames.add( currColHeader.name ) | |
currColHeader = currColHeader.right | |
return count | |
def ___High_Level_Logic___(self): pass | |
# ====================================== | |
# Modifies array; caller's job to make copy; also returns same array instance for convenience | |
# reminder: arrays now have header row & col | |
def deleteRow_np( npArray, rowOffset ): | |
debug = False | |
# if debug: print( f"Debug: deleteRow_np: target offset = {rowOffset}, \nvvv\n{npArray}" ) | |
if debug: print( f"Debug: deleteRow_np: target offset = {rowOffset}", flush=True ) | |
outArray = np.delete( npArray, rowOffset, 0 ) | |
# if debug: print( f"Debug: After: \nvvv\n{outArray}" ) | |
return outArray | |
# reminder: arrays now have header row & col | |
def deleteCol_np( npArray, colOffset ): | |
debug = False | |
if debug: print( f"Debug: deleteCol_np: target offset = {colOffset}, \nvvv\n{npArray}" ) | |
outArray = np.delete( npArray, colOffset, 1 ) | |
if debug: print( f"Debug: After: \nvvv\n{outArray}" ) | |
return outArray | |
""" | |
start w lowCount column | |
find intersecting rows | |
iterate through rows, foreach row | |
consider as part of the solution | |
get intersecting columns | |
^-- mark each for deletion | |
get intersecting rows from those cols | |
^-- delete all of those too! | |
-- | |
TODO: idea: be careful about empty board in 1st vs 2nd try/catch | |
TODO: generate moves.txt from more advanced model (non-extended, now w rot/refl metadata) | |
""" | |
""" | |
0: In one level/iteration of Solve | |
we choose one column | |
then foreach of the intersecting rows becomes a trial branch | |
1: If the matrix A has no columns, (empty, no rows either) | |
the current partial solution is a valid solution; terminate successfully. | |
2: Otherwise choose a column c (deterministically). | |
choose the column with the fewest 1's in it | |
if this turns out to be a column with ZERO 1's in it then | |
Bad solution, backtrack, throw exception, etc | |
3: Choose a row r such that Ar, c = 1 (nondeterministically). | |
3a: Choose all rows that intersect with that column | |
3b: Each row represents a branch to try and solf recursively | |
4: Include row r in the partial solution. | |
see also g_PendingSolutionPerLevel and printCandidateSolution() | |
5: For each column j such that Ar, j = 1, | |
for each row i such that Ai, j = 1, | |
delete row i from matrix A. | |
delete column j from matrix A. | |
6: Repeat this algorithm recursively on the reduced matrix A. | |
""" | |
# updated to work w header row & col | |
def solve_np( inArray, level ): | |
debug = False # local, even if global, bad idea!? | |
debug_display_matrix = False | |
if debug: print( f"Debug: solve_np: =============\n\tLevel {level}: inMatrix dims/shape={inArray.shape}", flush=True ) | |
if debug_display_matrix: | |
print( "Debug: solve_np: input array =\n", inArray, flush=True ) | |
origArray = inArray.copy() | |
# Step 1: if no columns (or rows), report possible solution | |
height = inArray.shape[ 0 ] # do we use these vars later? other than in debug statements?? | |
width = inArray.shape[ 1 ] | |
# allow room for header nodes | |
#if height<=1 or width<=1: | |
# raise EmptyBoard( f"rows={height}, cols={width}; likely indicates a Solution!" ) | |
# Change per suggestion here: https://stackoverflow.com/a/63066717/295802 | |
if width <= 1: | |
raise EmptyBoard_NoColumns( f"Empty Matrix (Columns); now {height} rows high x {width} cols wide, INCLUDING Headers; now data columns --> check for possible solution!" ) | |
if height <= 1: | |
raise EmptyBoard_NoRows( f"Empty Matrix (Rows); now {height} rows high x {width} cols wide, INCLUDING Headers; invalid solution, backtrack" ) | |
# Step 2: Choose a column | |
# this also gets a count and can throw exceptions | |
minCount = -1 | |
chosenColumn = -1 | |
# It's the 2nd try/except, aka try/catch, block that's got the the main logic | |
try: | |
( minCount, chosenColumn ) = getMinColumnCountAndColumnOffset_np( inArray ) | |
if minCount < 1: | |
raise ZeroValueColumn( f"0 count in column {chosenColumn}" ) | |
if debug: print( f'Debug: solve_np: Choosing Column "{chosenColumn}", Col HEADER "{inArray[0,chosenColumn]}", bit count = {minCount}', flush=True ) | |
# Bad News: invalid solution | |
except ZeroValueColumn as e1: | |
# there's nothing that can be done here, so return the exception | |
# Backtracking | |
# clear out state | |
g_PendingSolutionPerLevel[ level ] = None | |
if debug: print( "Debug: solve_np: BACKTRACK: Encountered ZeroValueColumn, cleared state and returning exception to caller" ) | |
raise e1 | |
# return | |
# Potential GOOD NEWS | |
except EmptyBoard_NoColumns as e2: | |
# resetStateForLevel( level ) | |
g_PendingSolutionPerLevel[ level ] = None | |
# We can't make a move, so must return | |
return | |
# Bad News | |
except EmptyBoard_NoRows as e3: | |
# resetStateForLevel( level ) | |
g_PendingSolutionPerLevel[ level ] = None | |
# We can't make a move, so must return | |
# We can't make a move, so must return | |
# raise e3 | |
return | |
# if debug: print( f'Debug: solve_np: At Level {level}, chose Column "{chosenColumn}" with {minCount} non-zero entries', flush=True ) | |
# Step 3: Get all intersecting rows | |
# --------------------------------------- | |
rowOffsetList = findIntersectingRowsForColByColOffset_np( inArray, chosenColumn ) | |
if debug: print( f"Debug: solve_np: Will iterate on intersecting Rows:\n\t{rowOffsetList}", flush=True ) | |
# Each of these is a possible solution branch | |
for i,candidateRowOffset in enumerate(rowOffsetList): | |
if debug: print( f'Debug: solve_np: -----\n\tIteration {i+1} of {len(rowOffsetList)}: Trying Solution with Row "{candidateRowOffset}", and will temp add to tentative solution list. (Level = {level})', flush=True ) | |
try: | |
# Create a board for this test | |
currCopyOfArray = origArray.copy() | |
# if debug: print( f"Debug: solve_np: BOUNDS checking: currCopyOfArray has {len(currCopyOfArray)} rows", flush=True ) | |
if debug: print( f'Debug: solve_np: Raw Row offset = "{candidateRowOffset}", Row HEADER = "{currCopyOfArray[candidateRowOffset,0]}"', flush=True) | |
# Tracking what we will eventually remove, | |
# but only once, and in high to low order. | |
deleteRowsSet = set() | |
deleteColsSet = set() | |
# Step 4: include this row as this level's potential solution | |
# ------------------------------------------------------------ | |
# g_PendingSolutionPerLevel[ level ] = candidateRowOffset | |
# Pull from our header column | |
g_PendingSolutionPerLevel[ level ] = origArray[ candidateRowOffset, 0 ] | |
# Step 5: Cover/DELETE all intersecting Columns and Rows and reduce the matrix | |
# PROBLEM STARTS HERE | |
# and on the first iteration | |
# too many things are deleted from the matrix | |
# ------------------------------------------------------------------------------- | |
# if debug: print( f"Debug: solve_np: Will now DELETE all intersecting Columns and Rows (at Level {level})" ) | |
# if debug: print( f"Debug: solve_np: BEFORE deletes: Now dims/shape={currCopyOfArray.shape}", flush=True ) | |
intersectingCols = findIntersectingColsForRowByRowOffset_np( currCopyOfArray, candidateRowOffset ) | |
if len(intersectingCols) < 1: | |
raise NoIntersectingCols( f"for candidateRowOffset={candidateRowOffset}" ) | |
deleteColsSet.update( intersectingCols ) | |
for c in intersectingCols: | |
intersectingRows = findIntersectingRowsForColByColOffset_np( currCopyOfArray, c ) | |
deleteRowsSet.update( intersectingRows ) | |
if len(intersectingRows) < 1: | |
raise NoIntersectingRows( f"For intersectingCols = {intersectingCols}") | |
# Now delete all the rows and cols, but in high to low order | |
# targetColsList = deleteColsSet[:] | |
targetColsList = list( deleteColsSet ) | |
targetColsList.sort() # <-- attemping work around as .reverse is not always sorted correctly | |
targetColsList.reverse() # in-place reverse sorting | |
if debug: print( f"Debug: solve_np: will DELETE Columns {targetColsList}") | |
for c in targetColsList: | |
# currCopyOfArray = np.delete( currCopyOfArray, c, 1 ) # "axis 1" = col | |
currCopyOfArray = deleteCol_np( currCopyOfArray, c ) | |
# ^-- TODO: doubt we need that | |
# targetRowsList = deleteRowsSet[:] | |
targetRowsList = list( deleteRowsSet ) | |
targetRowsList.sort() | |
targetRowsList.reverse() # in-place reverse sorting TODO: though reverse would work by itself | |
if debug: print( f"Debug: solve_np: will DELETE {len(targetRowsList):,} Rows...") | |
for r in targetRowsList: | |
# currCopyOfArray = np.delete( currCopyOfArray, c, 0 ) # "axis 0" = row | |
currCopyOfArray = deleteRow_np( currCopyOfArray, r ) | |
if debug: print( f"Debug: solve_np: AFTER deletes: Now dims/shape={currCopyOfArray.shape}", flush=True ) | |
# Step 6: Solve the Reduced Matrix | |
# --------------------------------------- | |
# print( f"Debug: solve_np: Since no exceptions so far, will recursively call solve with level+1 = {level+1}" ) | |
if debug: print( f"Debug: solve_np: Recursively calling solve with level+1 = {level+1}" ) | |
solve_np( currCopyOfArray, level + 1 ) | |
except (ZeroColumns, EmptyBoard_NoColumns): | |
print( "Debug: solve_np: POTENTIAL SOLUTION:" ) | |
printCandidateSolution() | |
continue | |
# return | |
except (ZeroValueColumn, EmptyBoard_NoRows) as e: | |
# reset state | |
g_PendingSolutionPerLevel[ level ] = None | |
# Backtrack | |
# return or continue | |
# We will have already backtracked once | |
# And another move inside this iteration loop might be OK | |
# It's not the "current" board at this level, but the reduced board of move we were trying | |
# that was no good | |
continue | |
except Exception as err: | |
# cleanup this state..., not much to do ??? | |
# the copy of the array we're using was already a burner, so let that fall out of scope and be collected, etc | |
g_PendingSolutionPerLevel[ level ] = None | |
raise err | |
# reset state | |
g_PendingSolutionPerLevel[ level ] = None | |
# end foreach Candidate Row | |
def ___Init_and_Generate_Board___(self): pass | |
# ======================================= | |
def getBlankPentominoesBoard( height, width ): | |
newBoard = [ [ ' ' for x in range(width)] for y in range(height) ] | |
return newBoard | |
# REMINDER to Caller: add +1 for row and col due to header row | |
def getBlankBoard_asNumPy( height, width ): | |
# newNumPyBoard = np.zeros( (height, width), dtype=np.bool_ ) | |
newNumPyBoard = np.zeros( (height, width), dtype=np.short ) | |
print( f"Debug: New numpy array is {newNumPyBoard.shape}" ) | |
return newNumPyBoard | |
def _getBlankMovesBoard_asBitmap( height, width ): | |
newBoard = [ [ Node(0) for x in range(width)] for y in range(height) ] | |
return newBoard | |
# | |
# step 1: read file in plain Python array... | |
# then we look at stats and request a numpy array | |
# need to know size first, so python first | |
def setupMovesBoard_np(): | |
dataArray = [] | |
with open( DATA_FILE, 'r' ) as fin: | |
while True: | |
line = fin.readline() | |
if line is None or line == "": | |
break | |
line = line.rstrip() | |
( rowName, rowBitsStr ) = line.split( '|' ) | |
g_allPossibleMoves.append( rowBitsStr ) | |
g_rowNames.append( rowName ) | |
g_rowNamesToBitMoves[ rowName ] = rowBitsStr | |
g_bitMovesToRowNames[ rowBitsStr ] = rowName | |
newRow = [] | |
for c in rowBitsStr: | |
value = int( c ) | |
newRow.append( value ) | |
dataArray.append( newRow ) | |
print( f'Debug: setupMovesBoard_np: file "{DATA_FILE}" had {len(dataArray):,} elements', flush=True ) | |
# Promote from ints to Nodes | |
# Also leaving room for header column and row | |
currNpArray = getBlankBoard_asNumPy( len(dataArray)+1, len(dataArray[0])+1 ) | |
npHeight = currNpArray.shape[0] | |
npWidth = currNpArray.shape[1] | |
# Do the headers first | |
# OK that the node at 0,0 is overwritten, both call that slot 0 anyway | |
for r in range(npHeight): | |
currNpArray[ r, 0 ] = r | |
for c in range(npWidth): | |
currNpArray[ 0, c ] = c | |
# Now do the data nodes | |
for r,rowData in enumerate( dataArray ): | |
for c,nodeData in enumerate( rowData ): | |
currValue = dataArray[ r ][ c ] | |
currNpArray[ r+1, c+1 ] = currValue | |
return currNpArray | |
# near bottom | |
def main(): | |
# Part 1: Generate all the valid Moves | |
# (it still writes and reads the file, cool for debugging) | |
# Part 2: Use the valid moves bitmask to solve Pentos | |
# | |
# Part 1: | |
# Generate all the moves | |
# (and write to a file) | |
# ------------------------ | |
moves = generateAllPossibleMoves_v2() | |
print( f"Debug: g_allPossibleMoves ({len(g_allPossibleMoves):,}) entries: (details commented out)" ) | |
writeMovesToFile() | |
# | |
# Part 2: | |
# Use the moves to solf the puzzle | |
# This is the "Dancing Links"/DLX stuff that I'm having trouble with | |
# Solve the puzzle | |
g_npOrigArray = setupMovesBoard_np() | |
print( f"Debug: main: Init array =\n{g_npOrigArray}" ) | |
try: | |
print( "Debug: main: Calling solve_np ...", flush=True ) | |
currArray = g_npOrigArray | |
solve_np( currArray, 0 ) | |
except EmptyBoard_NoColumns: # TODO: don't think we can get here, if so, could add _NoRows block too | |
if g_debug: print( "Main: Debug: about to print solution" ) | |
o.printCandidateSolution() | |
if __name__ == "__main__": | |
main() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment