@ -44,6 +44,18 @@ class Rewards:
@@ -44,6 +44,18 @@ class Rewards:
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
@ -51,10 +63,12 @@ class Field:
@@ -51,10 +63,12 @@ class Field:
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 . values [ : ] = " "
self . clear_field ( )
def __repr__ ( self ) :
result_str = " ⬛️ " * ( self . width + 2 ) + " \n "
@ -68,15 +82,114 @@ class Field:
@@ -68,15 +82,114 @@ class Field:
def clear_field ( self ) :
self . values [ : ] = " "
@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 )
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 )