def test_levels(self): #same orientation orientation = 'tall_wide' block1 = Block(block_mesh, orientation, (0, 0, 0)) block2 = Block(block_mesh, orientation, (2, 1, 0)) print(block1.get_top_level()) print(block2.get_top_level()) self.assertTrue(block1.get_top_level() == block2.get_top_level()) print(block1.get_bottom_level()) print(block2.get_bottom_level()) self.assertTrue(block1.get_bottom_level() == block2.get_bottom_level()) # differing orientations block1 = Block(block_mesh, 'flat_wide', ( 0, 0, 0)) block2 = Block(block_mesh, 'tall_thin', ( 5, 3, -7)) print(block1.get_top_level()) print(block2.get_top_level()) self.assertTrue(block1.get_top_level() == block2.get_top_level()) print(block1.get_bottom_level()) print(block2.get_bottom_level()) self.assertTrue(block1.get_bottom_level() > block2.get_bottom_level())
def is_stable(tower_state, new_block: Block): """ Boolean recursive function that calculates if the new block to be places in the scheme of the current block state will stand, or topple. :param tower_state: A state of the block tower. :param new_block: potential block to add, organized in a dictionary with keys as levels and values list of blocks level -> [block1, block2, ...] :return: True iff the new arrangement still stands """ bottom_level = new_block.get_bottom_level() top_level = new_block.get_top_level() # blocks_by_top_level= tower_state.get_blocks_by_top_level() # blocks_by_bottom_level= tower_state.get_blocks_by_bottom_level() # Initiate the relation to surrounding blocks in the tower # Short-circuit the expensive calculation if can be skipped. blocks_below = calculate_below(new_block, tower_state.get_by_top(bottom_level - 1)) \ if (bottom_level - 1) in tower_state \ else set() tower_state.set_blocks_below(new_block, blocks_below) blocks_above = calculate_above(new_block, tower_state.get_by_bottom(top_level + 1)) \ if (top_level + 1) in tower_state \ else set() tower_state.set_blocks_above(new_block, blocks_above) # Last attempt to find if this situation was already stored as a unstable combination if tower_state.is_bad_block( tower_state.stringify_block_neighbors(new_block)): return False # connect the new block to the blocks above and below by making changes to their neighbor setting. tower_state.connect_block_to_neighbors(new_block) if is_stable_helper((tower_state, new_block)): return True else: # We can stringify this block's failure, contingent on it's neighbors. # This will allow to quickly check in the future if this block is stable, relative to its surroundings tower_state.add_bad_block_state(new_block) # release the relation of this block on it neighbors tower_state.disconnect_block_from_neighbors(new_block) return False
def is_overlapping(block_tower, new_block: Block): """ Check if new block clashes (physically overlaps) with the rest of the block tower :param block_tower: A tower state :param new_block: either a new object or just it's string representation :return: """ if block_tower.is_bad_block(new_block): return True bottom_level: int = new_block.get_bottom_level() top_level: int = new_block.get_top_level() """ Only scan blocks with top levels above my bottom level. X - Block. B - Bottom layer. T - Top layer XXXXXXX new block XXXXXXX others TTTTTTTT others BBBBBBB in XXXXXXXX out TTTT XXXX """ for level in filter(lambda l: l >= bottom_level, block_tower.keys()): for other_block in block_tower.get_by_top(level): """ Diqualify for overlap any block with a bottom above our top X - Block. B - Bottom layer. T - Top layer others XXXXXXXX others XXXXX in XXXXXXXX out BBBBB TTTTTTT BBBBBBBB new block XXXXXXX XXXXXXX """ if other_block.get_bottom_level( ) <= top_level and other_block.is_overlapping(new_block): # perform a memoization of bad blocks we've seen block_tower.add_bad_block(new_block) return True return False
def get_spread(self, block1: Block, block2: Block): """ Retrieves the space between two blocks that can support blocks above, should their center of gravity sit within them. Spread should only be calculated between blocks that have already been recognized as close enough to hold another block above them, otherwise behavior is not defined, or in the best case an assertion will fail. This is garanteed by only calling get_spread on blocks both directly under the same piece. Spread is commutative :param block1: :param block2: :return: """ assert block1.get_top_level() == block2.get_top_level( ), str(block1) + str(block1.get_top_level()) + str(block2) + str( block2.get_top_level()) # a spread with yourself it the cells you cover if block1 == block2: return block1.get_cover_cells() ordered_pair = (block1, block2) if block1 < block2 else (block2, block1) if ordered_pair in self._spreads_memory: # shared with father return self._spreads_memory[ordered_pair] b1_cover_x = {cell[X] for cell in block1.get_cover_cells()} b2_cover_x = {cell[X] for cell in block2.get_cover_cells()} b1_cover_y = {cell[Y] for cell in block1.get_cover_cells()} b2_cover_y = {cell[Y] for cell in block2.get_cover_cells()} inter_x = b1_cover_x & b2_cover_x inter_y = b1_cover_y & b2_cover_y spread = set() spread |= block1.get_cover_cells() spread |= block2.get_cover_cells() if (inter_x): union_y = b1_cover_y | b2_cover_y min_y = int(min(union_y)) max_y = int(max(union_y)) for y in range(min_y, max_y + 1): for x in inter_x: spread.add((x, y)) elif (inter_y): union_x = b1_cover_x | b2_cover_x min_x = int(min(union_x)) max_x = int(max(union_x)) for x in range(min_x, max_x + 1): for y in inter_y: spread.add((x, y)) else: # no common pieces - skew lines """ X XXXXXXX XXXXXXX X X or X or X XXXXXXXX X X Relevant for flat pieces only. """ pass # See if the candidate blocks above can help increase spread candidate_blocks = self.get_blocks_above( block1) & self.get_blocks_above(block2) if candidate_blocks: flat_block = candidate_blocks.pop() center_x, center_y, _ = tuple(flat_block.get_cog()) skew_center = set() for cell in spread: new_cell = ((cell[X] + center_x) // 2, (cell[Y] + center_y) // 2) skew_center.add(new_cell) spread |= skew_center self._spreads_memory[ordered_pair] = spread return spread