Skip to content

Commit

Permalink
Fix broken tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
donkirkby committed Jan 5, 2025
1 parent 49e3e82 commit 948a57e
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 74 deletions.
59 changes: 34 additions & 25 deletions four_letter_blocks/block_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def __init__(self,
if 0 <= min_tries < tries:
self.stop_tries = tries - min_tries
self.is_tracing = False

# True if slots should be filled in random order, otherwise False if
# slots should be filled from top to bottom.
self.are_slots_shuffled = False

# True if self.state should be set, even with a partial filling
self.are_partials_saved = False

self.fewest_unused: int | None = None
self.slot_coverage = self.state

Expand Down Expand Up @@ -100,7 +108,7 @@ def calculate_max_shape_counts(self):
"""
# noinspection PyUnresolvedReferences
block_count = (self.state == 0).sum() // 4
block_count += 5*7 # Add flexibility to make packing easier.
block_count += 7 # Add flexibility to make packing easier.
multiplier = {'O': 4, 'S': 2, 'Z': 2, 'I': 2}
shape_names = Block.shape_rotation_names()
return {shape: math.ceil(multiplier.get(shape[0], 1) * block_count / 28)
Expand Down Expand Up @@ -159,7 +167,7 @@ def find_slots(self) -> dict[str, np.ndarray]:
slot_coverage += shape_coverage
slots[shape] = usable_slots
self.slot_coverage = slot_coverage
if slot_coverage.all():
if self.are_partials_saved or slot_coverage.all():
return slots

# Some unfilled spaces weren't covered by any usable slots, return empty.
Expand Down Expand Up @@ -234,24 +242,27 @@ def create_block(self, block_num):
block = Block(*squares)
return block

def fill(self,
shape_counts: typing.Counter[str],
are_slots_shuffled: bool = False,
are_partials_saved: bool = False) -> bool:
def fill(self, shape_counts: typing.Counter[str]) -> bool:
""" Fill in the current state with the given shapes.
Cycles through the available shapes in shape_counts, and tries them in
different positions, looking for the fewest rows. Set the current state
to a filled in copy, not changing the original.
Slots with least coverage are always filled first. If
self.are_slots_shuffled is True, then coverage ties are broken randomly,
otherwise ties are filled from top to bottom.
If self.are_partials_saved is True, then we don't cycle through options,
just make the first choice for each slot and return with self.state set.
:param shape_counts: number of blocks of each shape, disables rotation
if any of the shapes contain a letter and rotation number
:param are_slots_shuffled: True if slots should be filled in random
order, otherwise False if slots should be filled from top to bottom.
:param are_partials_saved: True if self.state should be set, even with
a partial filling
if any of the shapes contain a letter and rotation number. Adjusted
to remaining counts, if self.are_partials_saved is True.
:return: True, if successful, otherwise False.
"""
are_slots_shuffled = self.are_slots_shuffled
are_partials_saved = self.are_partials_saved
if self.tries == 0:
self.state = None
return False
Expand All @@ -260,6 +271,9 @@ def fill(self,
best_state = None
assert self.state is not None
start_state = self.state
if not sum(shape_counts.values()):
# Nothing to add!
best_state = start_state
next_block = self.find_next_block()
fewest_rows = start_state.shape[0]+1

Expand All @@ -284,7 +298,6 @@ def fill(self,
continue
# noinspection PyTypeChecker
shape_scores[shape] = -slot_count / target_count
is_filled = False
for shape, _score in shape_scores.most_common():
if not is_rotation_allowed:
slot_shapes = [shape]
Expand Down Expand Up @@ -326,9 +339,7 @@ def fill(self,
f'finished? {is_finished}')
print(self.display())
if not is_finished and self.tries != 0:
is_filled = self.fill(shape_counts,
are_slots_shuffled,
are_partials_saved)
is_filled = self.fill(shape_counts)
if not is_filled:
continue
used_rows = self.count_filled_rows()
Expand All @@ -352,11 +363,9 @@ def fill(self,
if are_partials_saved:
break
shape_counts[shape] = old_count
if is_filled:
return True
if ((not is_rotation_allowed or best_state is None) and
not are_partials_saved):
return False
# if ((not is_rotation_allowed or best_state is None) and
# not are_partials_saved):
# return False
# new_state = start_state.copy()
# # noinspection PyUnboundLocalVariable
# new_state[target_row, target_col] = 1 # gap
Expand All @@ -372,8 +381,8 @@ def fill(self,
self.state = None
return False

def find_next_block(self):
used_blocks = np.unique(self.state)
def find_next_block(self) -> int:
used_blocks = np.unique(self.state) # type: ignore
block: int
for i, block in enumerate(used_blocks[:-1]):
if block >= self.GAP and used_blocks[i + 1] != block + 1:
Expand Down Expand Up @@ -438,9 +447,9 @@ def count_filled_rows(self):

def random_fill(self, shape_counts: typing.Counter[str]):
""" Randomly place pieces from shape_counts on empty spaces. """
self.fill(shape_counts,
are_slots_shuffled=True,
are_partials_saved=False)
self.are_slots_shuffled = True
self.are_partials_saved = False
self.fill(shape_counts)

def flip(self) -> 'BlockPacker':
assert self.state is not None
Expand Down
2 changes: 2 additions & 0 deletions four_letter_blocks/double_block_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def fill(self) -> bool:
shape_scores[shape] = -slot_count / target_count

start_state1 = packer1.state
assert start_state1 is not None
start_state2 = packer2.state
assert start_state2 is not None
next_block = packer1.find_next_block()
for shape1, _score in shape_scores.most_common():
shape2 = flipped_shape_names[shape1]
Expand Down
22 changes: 17 additions & 5 deletions four_letter_blocks/evo_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,11 @@ def pair(self, other, pair_params):
mover2.move(i2, j2)
packer = BlockPacker(start_state=new_state)
packer.force_fours = True
packer.random_fill(mover1.shape_counts)
packer.are_slots_shuffled = True
packer.are_partials_saved = True
packer.fill(mover1.shape_counts)

assert packer.state is not None
return Packing(dict(state=packer.state,
shape_counts=mover1.shape_counts,
can_rotate=can_rotate,
Expand All @@ -117,6 +121,8 @@ def mutate(self, mutate_params) -> None:
can_rotate: bool = self.value['can_rotate']
block_packer = BlockPacker(start_state=state)
block_packer.force_fours = True
block_packer.are_partials_saved = True
block_packer.are_slots_shuffled = True
grid_size = state.shape[0]
gaps = np.argwhere(state == 0)
if gaps.size > 0:
Expand Down Expand Up @@ -149,8 +155,9 @@ def mutate(self, mutate_params) -> None:
if remove_count == 0:
break

block_packer.random_fill(shape_counts)
block_packer.fill(shape_counts)

assert block_packer.state is not None
self.value = dict(state=block_packer.state,
shape_counts=shape_counts,
can_rotate=can_rotate)
Expand All @@ -161,7 +168,11 @@ def _random_init(self, init_params: dict):
can_rotate = all(len(shape) == 1 for shape in shape_counts)
block_packer = BlockPacker(start_state=start_state)
block_packer.force_fours = True
block_packer.random_fill(shape_counts)
block_packer.are_slots_shuffled = True
block_packer.are_partials_saved = True
block_packer.fill(shape_counts)

assert block_packer.state is not None
return dict(state=block_packer.state,
shape_counts=shape_counts,
can_rotate=can_rotate)
Expand Down Expand Up @@ -270,6 +281,7 @@ def __init__(self,
start_state)
self.is_logging = False
self.epochs = 100
self.pool_size = 1000
self.current_epoch = 0
self.shape_counts: typing.Counter[str] = Counter()
self.evo: Evolution | None = None
Expand All @@ -287,10 +299,10 @@ def setup(self,
fitness_calculator.summaries.clear()

self.evo = Evolution(
pool_size=1000,
pool_size=self.pool_size,
fitness=fitness_calculator.calculate,
individual_class=Packing,
n_offsprings=200,
n_offsprings=self.pool_size // 5,
pair_params=None,
mutate_params=None,
init_params=init_params,
Expand Down
3 changes: 2 additions & 1 deletion four_letter_blocks/puzzle_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ def __init__(self,
self.block_packer = block_packer
else:
self.block_packer = BlockPacker(16, 20, # Game Crafter cutout size
tries=10_000)
tries=10_000,
min_tries=1)
self.front_blocks: typing.Dict[
str,
typing.List[Block | None]] = defaultdict(list)
Expand Down
51 changes: 29 additions & 22 deletions tests/test_block_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ def test_fill_two_blocks():
width, height = 5, 4
shape_counts = Counter('LO')
expected_display = dedent("""\
..ABB
AAABB
AA..B
AABBB
.....
.....""")
packer = BlockPacker(width, height)
Expand All @@ -135,8 +135,8 @@ def test_fill_two_blocks_force_fours():
width, height = 5, 4
shape_counts = Counter('LO')
expected_display = dedent("""\
AAABB
A..BB
AABBB
AAB..
.....
.....""")
packer = BlockPacker(width, height)
Expand Down Expand Up @@ -196,7 +196,7 @@ def test_no_rotations_needs_gap():
width = height = 5
shape_counts = Counter(('O', 'J3', 'O'))
expected_display = dedent("""\
AA#CC
AA.CC
AABCC
..BBB
.....
Expand All @@ -217,7 +217,9 @@ def test_random_fill():
.....
..##.""")
packer = BlockPacker(start_text=start_text)
packer.random_fill(shape_counts)
packer.are_slots_shuffled = True
packer.are_partials_saved = True
packer.fill(shape_counts)

assert 1 <= shape_counts['O'] <= 3

Expand All @@ -231,8 +233,10 @@ def test_random_fill_lower_numbers():
..#..
..DDD
..##D""")
packer = BlockPacker(start_text=start_text)
packer.random_fill(shape_counts)
packer = BlockPacker(start_text=start_text, tries=100, min_tries=1)
packer.are_slots_shuffled = True
packer.are_partials_saved = True
packer.fill(shape_counts)

assert 2 <= shape_counts['O'] <= 3
assert packer.state.max() == 5
Expand All @@ -245,7 +249,9 @@ def test_random_fill_tries_multiple_shapes():
AA...B
AA.BBB""")
packer = BlockPacker(start_text=start_text)
packer.random_fill(shape_counts)
packer.are_slots_shuffled = True
packer.are_partials_saved = True
packer.fill(shape_counts)

assert shape_counts['L'] == 0

Expand All @@ -254,7 +260,9 @@ def test_random_fill_no_gaps():
for _ in range(100):
shape_counts = Counter({'O': 1})
packer = BlockPacker(2, 2)
packer.random_fill(shape_counts)
packer.are_slots_shuffled = True
packer.are_partials_saved = True
packer.fill(shape_counts)

assert shape_counts['O'] == 0
assert packer.state.max() == 2
Expand Down Expand Up @@ -356,10 +364,10 @@ def test_fill_with_split_row():
AA.
AA.
...
BBB
.B.
CC.
CC.""")
BB.
BB.
CCC
.C.""")
packer = BlockPacker(width, height, split_row=3, tries=100)
packer.fill(shape_counts)

Expand All @@ -375,7 +383,7 @@ def test_fill_overflow():

assert len(packer.positions['O']) == 254

shape_counts['O'] += 1
shape_counts = Counter({'O': 255})
packer = BlockPacker(256, 4, tries=500)
with pytest.raises(ValueError, match='Maximum 254 blocks in packer.'):
packer.fill(shape_counts)
Expand Down Expand Up @@ -490,8 +498,9 @@ def test_shape_counts_7x7():
.......
...#...
#.....#"""))
expected_shape_counts = {name: 1 for name in Block.shape_names()}
expected_shape_counts['O'] = 2
expected_shape_counts = {
name: 3 if name == 'O' else 2 if name[0] in 'ISZ' else 1
for name in Block.shape_rotation_names()}

shape_counts = packer.calculate_max_shape_counts()

Expand All @@ -509,11 +518,9 @@ def test_shape_counts_9x9():
.........
...#.....
...#....."""))
expected_shape_counts = {name: 1 for name in Block.shape_names()}
expected_shape_counts['O'] = 3
for name in expected_shape_counts:
if name[0] in 'ISZ':
expected_shape_counts[name] = 2
expected_shape_counts = {
name: 4 if name == 'O' else 2 if name[0] in 'ISZ' else 1
for name in Block.shape_rotation_names()}

shape_counts = packer.calculate_max_shape_counts()

Expand Down
4 changes: 3 additions & 1 deletion tests/test_evo_packer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ def test_no_rotations():
DA#EC
DEEEC
DD##C""")
packer = EvoPacker(start_text=start_text)
packer = EvoPacker(start_text=start_text, tries=100, min_tries=1)
packer.is_logging = False
packer.epochs = 2
packer.pool_size = 100

is_filled = packer.fill(shape_counts)
packer.sort_blocks()
Expand Down
10 changes: 4 additions & 6 deletions tests/test_puzzle_pair.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,9 +497,8 @@ def test_pack_greetings():
min_tries=1)
back_shapes = packer.calculate_max_shape_counts()
packer.force_fours = True
packer.fill(back_shapes,
are_slots_shuffled=True,
are_partials_saved=False)
packer.are_slots_shuffled = True
packer.fill(back_shapes)
if not packer.is_full:
print('First packer failed.')
continue
Expand All @@ -515,9 +514,8 @@ def test_pack_greetings():
tries=1000,
min_tries=1)
packer2.force_fours = True
packer2.fill(target_shapes,
are_partials_saved=False,
are_slots_shuffled=True)
packer2.are_slots_shuffled = True
packer2.fill(target_shapes)
packer.sort_blocks()
packer2.sort_blocks()
display1 = packer.display()
Expand Down
Loading

0 comments on commit 948a57e

Please sign in to comment.