|
|
import dataclasses |
|
|
from dataclasses import dataclass |
|
|
from typing import Dict, List, Optional |
|
|
|
|
|
import numpy as np |
|
|
|
|
|
color_mapper = { |
|
|
"Y": "🟨", |
|
|
"G": "🟩", |
|
|
"B": "🟦", |
|
|
"R": "🟥", |
|
|
"P": "🟪", |
|
|
"O": "🟧", |
|
|
"W": "⬜", |
|
|
} |
|
|
|
|
|
|
|
|
@dataclass(frozen=True) |
|
|
class Block: |
|
|
block_id: int |
|
|
width: int |
|
|
height: int |
|
|
color: str |
|
|
values: np.array # Note: first row is the bottom, values are flipped when printing the result |
|
|
|
|
|
def __post_init__(self): |
|
|
if self.width != len(self.values[0]) or self.height != len(self.values): |
|
|
raise Exception( |
|
|
f"This block with width {self.width}, height: {self.height} and values {self.values} does not exist" |
|
|
) |
|
|
|
|
|
def __repr__(self): |
|
|
result_str = "Block:\n" |
|
|
for row in reversed(self.values): |
|
|
for item in row: |
|
|
result_str += color_mapper[item] if item else "⬜️" |
|
|
result_str += "\n" |
|
|
return result_str |
|
|
|
|
|
|
|
|
@dataclass(frozen=True) |
|
|
class Rewards: |
|
|
points: Dict[str, int] |
|
|
multiplication_factors: Dict[str, int] |
|
|
|
|
|
|
|
|
@dataclass() |
|
|
class Solution: |
|
|
"""A solution comprises two lists that describe which blocks are dropped in what order and where |
|
|
- order: order of the list |
|
|
- which blocks: determined by block_ids |
|
|
- where: most left part of the block is dropped at block_position |
|
|
""" |
|
|
|
|
|
block_ids: List[int] = dataclasses.field(default_factory=list) |
|
|
block_positions: List[int] = dataclasses.field(default_factory=list) |
|
|
|
|
|
|
|
|
@dataclass() |
|
|
class Field: |
|
|
width: int |
|
|
height: int |
|
|
values: Optional[ |
|
|
np.ndarray |
|
|
] = None # Note: first row is the bottom, values are flipped when printing the result |
|
|
block_ids: Optional[List[int]] = None |
|
|
block_positions: Optional[List[int]] = None |
|
|
|
|
|
def __post_init__(self): |
|
|
self.values = np.empty((self.height, self.width), dtype=str) |
|
|
self.clear_field() |
|
|
|
|
|
def __repr__(self): |
|
|
result_str = "⬛️" * (self.width + 2) + "\n" |
|
|
for row in reversed(self.values): |
|
|
result_str += "⬛️" |
|
|
for item in row: |
|
|
result_str += color_mapper[item] if item else "⬜" |
|
|
result_str += "⬛️\n" |
|
|
result_str += "⬛️" * (self.width + 2) |
|
|
return result_str |
|
|
|
|
|
def clear_field(self): |
|
|
self.values[:] = "" |
|
|
self.block_ids = [] |
|
|
self.block_positions = [] |
|
|
|
|
|
def fit(self, 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 > self.width |
|
|
out_of_bounds_on_the_top = y + block.height > self.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 = self.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(self, 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 = self.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)] |
|
|
self.values[update_y][x: x + block.width] = new_row |
|
|
|
|
|
def drop_block(self, 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 |
|
|
""" |
|
|
if block.block_id in self.block_ids: |
|
|
raise Exception( |
|
|
f"Block {block.block_id} was already used. You can only use each block once." |
|
|
) |
|
|
y = self.height - block.height |
|
|
fits_somewhere = False |
|
|
while self.fit(block, x, y): |
|
|
fits_somewhere = True |
|
|
y -= 1 |
|
|
|
|
|
if not fits_somewhere: |
|
|
raise Exception(f"{block} does not fit on x: {x}") |
|
|
self.update(block, x, y + 1) |
|
|
self.block_ids.append(block.block_id) |
|
|
self.block_positions.append(x) |
|
|
|
|
|
def drop_blocks(self, 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): |
|
|
self.drop_block(blocks[block_id], block_position) |
|
|
|
|
|
def score_row(self, row: np.array, rewards: Rewards) -> int: |
|
|
def color_points(row: list, rewards: Rewards) -> int: |
|
|
score = 0 |
|
|
for color in row: |
|
|
if color == "": |
|
|
score -= 1 |
|
|
else: |
|
|
score += rewards.points[color] |
|
|
return score |
|
|
|
|
|
def multiplication_factor_for_full_color_row(row: list, rewards: Rewards) -> int: |
|
|
if row[0] != "" and len(set(row)) == 1: |
|
|
return rewards.multiplication_factors[row[0]] |
|
|
return 1 |
|
|
|
|
|
full_row = not any(row == "") |
|
|
empty_row = "".join(row) == "" |
|
|
score = 0 |
|
|
if not empty_row: |
|
|
score += color_points(row, rewards) |
|
|
if full_row: |
|
|
score *= multiplication_factor_for_full_color_row(row, rewards) |
|
|
return score |
|
|
|
|
|
def score_solution(self, rewards: Rewards) -> int: |
|
|
score = 0 |
|
|
for row in self.values: |
|
|
score += self.score_row(row, rewards) |
|
|
return score |
|
|
|
|
|
def write_solution(self, 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( |
|
|
self.block_ids, self.block_positions |
|
|
): |
|
|
print(f"{block_id} {block_position}", file=fp)
|
|
|
|