You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
4.7 KiB
124 lines
4.7 KiB
from pathlib import Path |
|
from typing import List, Tuple |
|
|
|
import numpy as np |
|
|
|
from common.data_models import Block, Field, Rewards, Solution |
|
|
|
|
|
def parse_puzzle(filename: str) -> Tuple[Field, List[Block], Rewards]: |
|
"""Parse puzzle specification into a Field, Block list and Rewards.""" |
|
lines = Path(filename).read_text().splitlines() |
|
|
|
width, height = map(int, lines.pop(0).split()) |
|
field = Field(width, height) |
|
|
|
nr_blocks, nr_rewards = map(int, lines.pop(0).split()) |
|
|
|
blocks = [] |
|
for block_id in range(nr_blocks): |
|
width, height, color = lines.pop(0).split() |
|
width, height = int(width), int(height) |
|
values = [ |
|
"" if v == "0" else v for v in lines.pop(0).split() |
|
] # replace 0 with empty string for empty value |
|
values = np.reshape(values, (height, width)) |
|
blocks.append(Block(block_id, width, height, color, values)) |
|
|
|
points = {} |
|
multiplication_factors = {} |
|
for i in range(nr_rewards): |
|
color, point, multiplication_factor = lines.pop(0).split() |
|
points[color] = int(point) |
|
multiplication_factors[color] = int(multiplication_factor) |
|
|
|
return field, blocks, Rewards(points, multiplication_factors) |
|
|
|
def write_solution(solution: Solution, filename: str) -> None: |
|
"""Write an output file containing a solution block list""" |
|
with open(filename, "w") as fp: |
|
for block_id, block_position in zip( |
|
solution.block_ids, solution.block_positions |
|
): |
|
print(f"{block_id} {block_position}", file=fp) |
|
|
|
|
|
def parse_solution(filename: str) -> Solution: |
|
"""Read and parse a solution from file""" |
|
fp = open(filename, "r") |
|
lines = fp.readlines() |
|
solution = Solution() |
|
for line in lines: |
|
block_id, block_position = map(int, line.split()) |
|
solution.block_ids.append(block_id) |
|
solution.block_positions.append(block_position) |
|
|
|
return solution |
|
|
|
|
|
def fit(field: Field, block: Block, x: int, y: int) -> bool: |
|
"""Check if the block fits with the left bottom on (x, y) |
|
|
|
field: where field.values is an array where first row is the bottom of the playing field |
|
block: where block.values is an array where first row is the bottom of the block |
|
x, y: start position of left bottom of the block's bounding box |
|
""" |
|
out_of_bounds_on_the_right = x + block.width > field.width |
|
out_of_bounds_on_the_top = y + block.height > field.height |
|
out_of_bounds_on_the_left = x < 0 |
|
out_of_bounds_on_the_bottom = y < 0 |
|
if ( |
|
out_of_bounds_on_the_right |
|
or out_of_bounds_on_the_top |
|
or out_of_bounds_on_the_bottom |
|
or out_of_bounds_on_the_left |
|
): |
|
return False |
|
|
|
# select area of the field where the block should fit |
|
selected_field_area = field.values[y : y + block.height, x : x + block.width] |
|
for block_row, field_row in zip(block.values, selected_field_area): |
|
for block_value, field_value in zip(block_row, field_row): |
|
# block fits if: field value or block value is an empty string |
|
if block_value and field_value: |
|
return False |
|
return True |
|
|
|
|
|
def update(field: Field, block: Block, x: int, y: int) -> None: |
|
"""Update the field by dropping the block on position (x,y)""" |
|
for update_y, block_row in zip(range(y, y + block.height), block.values): |
|
field_row = field.values[update_y][x : x + block.width] |
|
|
|
# make sure we don't overwrite an existing field block if the block |
|
# has an empty space where in the field the value is filled |
|
new_row = [f if f else b for f, b in zip(field_row, block_row)] |
|
field.values[update_y][x : x + block.width] = new_row |
|
|
|
|
|
def drop_block(field: Field, block: Block, x: int) -> None: |
|
"""Try to add the block on position x; |
|
If it fits then update the field, if not then raise exception. |
|
|
|
x: position of the most left part of the block |
|
""" |
|
y = field.height - block.height |
|
fits_somewhere = False |
|
while fit(field, block, x, y): |
|
fits_somewhere = True |
|
y -= 1 |
|
|
|
if not fits_somewhere: |
|
raise Exception(f"{block} does not fit on x: {x}") |
|
update(field, block, x, y + 1) |
|
|
|
|
|
def drop_blocks(field: Field, blocks: List[Block], solution: Solution) -> None: |
|
"""Drop the blocks for the solution in the playing field""" |
|
if len(solution.block_ids) != len(set(solution.block_ids)): |
|
raise Exception("You can only use each block once") |
|
if (max(solution.block_ids) >= len(blocks)) or (min(solution.block_ids) < 0): |
|
raise Exception("You used a block id that doesn't exist") |
|
|
|
for block_id, block_position in zip(solution.block_ids, solution.block_positions): |
|
drop_block(field, blocks[block_id], block_position)
|
|
|