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. 139
      common/data_models.py
  2. 48
      common/parsing.py
  3. 155
      common/score_evaluation.py
  4. 79
      quick_start.ipynb

139
common/data_models.py

@ -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)

48
common/parsing.py

@ -0,0 +1,48 @@ @@ -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 @@ @@ -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 @@ @@ -108,7 +108,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"metadata": {
"id": "5nljvNsQM-S7"
},
@ -125,12 +125,8 @@ @@ -125,12 +125,8 @@
")\n",
"\n",
"# Puzzle helper functions\n",
"from common.score_evaluation import (\n",
"from common.parsing import (\n",
" parse_puzzle,\n",
" write_solution,\n",
" drop_block,\n",
" drop_blocks,\n",
" score_solution,\n",
")"
]
},
@ -145,7 +141,7 @@ @@ -145,7 +141,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"metadata": {
"id": "ZAimvAxqhrBy"
},
@ -179,7 +175,7 @@ @@ -179,7 +175,7 @@
},
{
"cell_type": "code",
"execution_count": 24,
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
@ -187,21 +183,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -318,7 +293,7 @@
},
"outputs": [],
"source": [
"write_solution(solution, \"solution.txt\")"
"field.write_solution(\"solution.txt\")"
]
},
{
@ -361,7 +336,7 @@ @@ -361,7 +336,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.12"
"version": "3.9.10"
}
},
"nbformat": 4,

Loading…
Cancel
Save