Drop some Blox! The one who drops in the most efficient way wins! 🏆
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.
 
 

195 lines
6.9 KiB

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)