From d38c2b692d0f05daa5d9758ed568d35f827c8e0f Mon Sep 17 00:00:00 2001 From: ykerus <48921025+ykerus@users.noreply.github.com> Date: Sun, 10 Apr 2022 13:52:48 +0200 Subject: [PATCH 1/2] Add functions to field class - put all functions related to field under field class - drop_block now stores which block was used and where - solutions file can directly be generated from field object - notebook adjusted accordingly --- common/data_models.py | 139 +++++++++++++++++++++++++++++++++---- common/score_evaluation.py | 107 ---------------------------- quick_start.ipynb | 77 +++++++------------- 3 files changed, 152 insertions(+), 171 deletions(-) diff --git a/common/data_models.py b/common/data_models.py index 9094df1..0e1234c 100644 --- a/common/data_models.py +++ b/common/data_models.py @@ -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: 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: 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) diff --git a/common/score_evaluation.py b/common/score_evaluation.py index e74cf70..01e36ff 100644 --- a/common/score_evaluation.py +++ b/common/score_evaluation.py @@ -34,14 +34,6 @@ def parse_puzzle(filename: str) -> Tuple[Field, List[Block], Rewards]: 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""" @@ -54,102 +46,3 @@ def parse_solution(filename: str) -> Solution: 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) - -def score_row(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(field: Field, rewards: Rewards) -> int: - score = 0 - for row in field.values: - score += score_row(row, rewards) - return score - diff --git a/quick_start.ipynb b/quick_start.ipynb index ce25c2f..fc01372 100644 --- a/quick_start.ipynb +++ b/quick_start.ipynb @@ -108,7 +108,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "id": "5nljvNsQM-S7" }, @@ -127,10 +127,6 @@ "# Puzzle helper functions\n", "from common.score_evaluation import (\n", " parse_puzzle,\n", - " write_solution,\n", - " drop_block,\n", - " drop_blocks,\n", - " score_solution,\n", ")" ] }, @@ -145,7 +141,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "id": "ZAimvAxqhrBy" }, @@ -179,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" @@ -187,21 +183,7 @@ "id": "tSPKZgB3M3-H", "outputId": "18d7daf0-a1ac-46b3-8517-0ce51e5ff481" }, - "outputs": [ - { - "data": { - "text/plain": [ - "Block:\n", - "🟧🟧🟧\n", - "🟧⬜️🟧\n", - "⬜️⬜️🟧" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "first_block = blocks[0]\n", "first_block" @@ -218,14 +200,31 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": { "id": "2f_a4iRNN22F" }, "outputs": [], "source": [ "x = 0\n", - "drop_block(field, first_block, x)" + "field.drop_block(first_block, x)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The order of dropped blocks and their corresponding positions are stored.\n", + "These are used to create your solutions file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "field.block_ids, field.block_positions" ] }, { @@ -265,31 +264,7 @@ "metadata": {}, "outputs": [], "source": [ - "score_solution(field, rewards)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "SBLSamhqP8rq" - }, - "source": [ - "Create a solution object:\n", - "\n", - "→ Dropping block `0` at index `0`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "2a0_-SciP8zG" - }, - "outputs": [], - "source": [ - "block_ids = [0]\n", - "block_positions = [0]\n", - "solution = Solution(block_ids=block_ids, block_positions=block_positions)" + "field.score_solution(rewards)" ] }, { @@ -318,7 +293,7 @@ }, "outputs": [], "source": [ - "write_solution(solution, \"solution.txt\")" + "field.write_solution(\"solution.txt\")" ] }, { @@ -361,7 +336,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.12" + "version": "3.9.10" } }, "nbformat": 4, From 4108a337ae7392222444c9bbc97946ece44273b6 Mon Sep 17 00:00:00 2001 From: ykerus <48921025+ykerus@users.noreply.github.com> Date: Sun, 10 Apr 2022 14:02:04 +0200 Subject: [PATCH 2/2] Change filename --- common/{score_evaluation.py => parsing.py} | 0 quick_start.ipynb | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename common/{score_evaluation.py => parsing.py} (100%) diff --git a/common/score_evaluation.py b/common/parsing.py similarity index 100% rename from common/score_evaluation.py rename to common/parsing.py diff --git a/quick_start.ipynb b/quick_start.ipynb index fc01372..19e9413 100644 --- a/quick_start.ipynb +++ b/quick_start.ipynb @@ -125,7 +125,7 @@ ")\n", "\n", "# Puzzle helper functions\n", - "from common.score_evaluation import (\n", + "from common.parsing import (\n", " parse_puzzle,\n", ")" ]