Browse Source

Merge pull request #1 from godatadriven/improve-field-class

Improve field class
main
ykerus 4 years ago committed by GitHub
parent
commit
18f4de9f2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 133
      common/data_models.py
  2. 48
      common/parsing.py
  3. 155
      common/score_evaluation.py
  4. 79
      quick_start.ipynb

133
common/data_models.py

@ -44,6 +44,18 @@ class Rewards:
multiplication_factors: 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() @dataclass()
class Field: class Field:
width: int width: int
@ -51,10 +63,12 @@ class Field:
values: Optional[ values: Optional[
np.ndarray np.ndarray
] = None # Note: first row is the bottom, values are flipped when printing the result ] = 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): def __post_init__(self):
self.values = np.empty((self.height, self.width), dtype=str) self.values = np.empty((self.height, self.width), dtype=str)
self.values[:] = "" self.clear_field()
def __repr__(self): def __repr__(self):
result_str = "" * (self.width + 2) + "\n" result_str = "" * (self.width + 2) + "\n"
@ -68,15 +82,114 @@ class Field:
def clear_field(self): def clear_field(self):
self.values[:] = "" 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)
@dataclass() field: where field.values is an array where first row is the bottom of the playing field
class Solution: block: where block.values is an array where first row is the bottom of the block
"""A solution comprises two lists that describe which blocks are dropped in what order and where x, y: start position of left bottom of the block's bounding box
- order: order of the list
- which blocks: determined by block_ids
- where: most left part of the block is dropped at block_position
""" """
out_of_bounds_on_the_right = x + block.width > self.width
block_ids: List[int] = dataclasses.field(default_factory=list) out_of_bounds_on_the_top = y + block.height > self.height
block_positions: List[int] = dataclasses.field(default_factory=list) 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)

48
common/parsing.py

@ -0,0 +1,48 @@
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 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

155
common/score_evaluation.py

@ -1,155 +0,0 @@
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)
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

79
quick_start.ipynb

@ -108,7 +108,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 2, "execution_count": null,
"metadata": { "metadata": {
"id": "5nljvNsQM-S7" "id": "5nljvNsQM-S7"
}, },
@ -125,12 +125,8 @@
")\n", ")\n",
"\n", "\n",
"# Puzzle helper functions\n", "# Puzzle helper functions\n",
"from common.score_evaluation import (\n", "from common.parsing import (\n",
" parse_puzzle,\n", " parse_puzzle,\n",
" write_solution,\n",
" drop_block,\n",
" drop_blocks,\n",
" score_solution,\n",
")" ")"
] ]
}, },
@ -145,7 +141,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 3, "execution_count": null,
"metadata": { "metadata": {
"id": "ZAimvAxqhrBy" "id": "ZAimvAxqhrBy"
}, },
@ -179,7 +175,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 24, "execution_count": null,
"metadata": { "metadata": {
"colab": { "colab": {
"base_uri": "https://localhost:8080/" "base_uri": "https://localhost:8080/"
@ -187,21 +183,7 @@
"id": "tSPKZgB3M3-H", "id": "tSPKZgB3M3-H",
"outputId": "18d7daf0-a1ac-46b3-8517-0ce51e5ff481" "outputId": "18d7daf0-a1ac-46b3-8517-0ce51e5ff481"
}, },
"outputs": [ "outputs": [],
{
"data": {
"text/plain": [
"Block:\n",
"🟧🟧🟧\n",
"🟧⬜🟧\n",
"⬜🟧"
]
},
"execution_count": 24,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [ "source": [
"first_block = blocks[0]\n", "first_block = blocks[0]\n",
"first_block" "first_block"
@ -218,14 +200,31 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 25, "execution_count": null,
"metadata": { "metadata": {
"id": "2f_a4iRNN22F" "id": "2f_a4iRNN22F"
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"x = 0\n", "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": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"score_solution(field, rewards)" "field.score_solution(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)"
] ]
}, },
{ {
@ -318,7 +293,7 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"write_solution(solution, \"solution.txt\")" "field.write_solution(\"solution.txt\")"
] ]
}, },
{ {
@ -361,7 +336,7 @@
"name": "python", "name": "python",
"nbconvert_exporter": "python", "nbconvert_exporter": "python",
"pygments_lexer": "ipython3", "pygments_lexer": "ipython3",
"version": "3.8.12" "version": "3.9.10"
} }
}, },
"nbformat": 4, "nbformat": 4,

Loading…
Cancel
Save