Skip to content

Commit

Permalink
Implement resource distribution calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivanov1ch committed Apr 8, 2021
1 parent 12eca77 commit e27edfb
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 133 deletions.
130 changes: 1 addition & 129 deletions catan/balance/balance_functions.py
Original file line number Diff line number Diff line change
@@ -1,135 +1,7 @@
import math
from catan.balance.resource_distribution import measure_resource_distribution


# Inspired by Board Game Analysis's blog post: https://www.boardgameanalysis.com/what-is-a-balanced-catan-board/

def calculate_balance(board):
return measure_resource_distribution(board)


# Divides the island into equal parts and analyzes the frequency of available resources, three times
def measure_resource_distribution(board):
# For convenience
settlements = board.settlement_locations
# Calculate for horizontal division (horizontal line through the center of the board) - split into two groups
top_half = []
bottom_half = []

for row in range(math.ceil(len(settlements) / 2)):
top_row = settlements[row]
bottom_row = settlements[len(settlements) - 1 - row]

# Due to the symmetric nature of a Catan board, top_row and bottom_row will have the same lengths
for index in range(len(top_row)):
top_half.append(top_row[index])
bottom_half.append(bottom_row[index])

# Calculate the resource distribution score for this horizontal division
horizontal_division_score = calculate_resource_distribution_score(top_half, bottom_half)

# Calculate for the positive-slope diagonal division (from bottom left to top right)
left_half_positive = []
right_half_positive = []

# And also for the negative-slope diagonal division (from top left to bottom right)
left_half_negative = []
right_half_negative = []

# Populate these arrays
# This is essentially a row-by-row operation, as there is not a solid coordinate system to work with

# A dictionary mapping each index in settlements to how many SettlementLocations lie to the right of the
# positive-slope dividing line or to the left of the negative-slope dividing line in the TOP HALF of the board.
# This will be used to call fill_arrays_from_sides and populate the left and right half arrays.
# Only indices 0-5 are mapped because the vertical symmetry is utilized to manage the other half
row_to_num_on_side = {
0: 0,
1: 1,
2: 1,
3: 2,
4: 2,
5: 3
}

# Use the items in the dictionary to call fill_arrays_from_sides
for settlement_row, num_right in row_to_num_on_side.items():
fill_arrays_from_sides(left_half_positive, right_half_positive, left_half_negative, right_half_negative,
settlements, settlement_row, num_right)

# Calculate the resource distribution score for these divisions
positive_slope_division_score = calculate_resource_distribution_score(left_half_positive, right_half_positive)
negative_slope_division_score = calculate_resource_distribution_score(left_half_negative, right_half_negative)

# Calculate and return the final result by summing the scores for each of the 3 divisions
return horizontal_division_score + positive_slope_division_score + negative_slope_division_score


# Adds SettlementLocations from settlements (A Board's settlement_locations) in the row in the TOP HALF of the board,
# settlement_row, to the left and right-side groups for the positive-slope diving line, left_arr_pos and right_arr_pos,
# of SettlementLocations. num_on_side is an integer, indicating how many SettlementLocations on this row (in the TOP
# HALF) are on the right side of the positive-slope line or on the left side of the negative-slope line. This method
# utilizes the vertical symmetry of the board to also add the SettlementLocations from the row vertically opposite to
# settlement_row.
def fill_arrays_from_sides(left_arr_pos, right_arr_pos, left_arr_neg, right_arr_neg, settlements,
settlement_row, num_on_side):
bottom_settlement_row = len(settlements) - 1 - settlement_row

for col in range(len(settlements[settlement_row]) - num_on_side):
bottom_col = len(settlements[bottom_settlement_row]) - 1 - col

# Fill for positive-slope dividing line
left_arr_pos.append(settlements[settlement_row][col])
right_arr_pos.append(settlements[bottom_settlement_row][bottom_col])

# Fill in reverse for negative-slope dividing line
right_arr_neg.append(settlements[settlement_row][col])
left_arr_neg.append(settlements[bottom_settlement_row][bottom_col])

for col in range(num_on_side):
# Fill for positive-slope dividing line
left_arr_pos.append(settlements[bottom_settlement_row][col])
right_arr_pos.append(settlements[settlement_row][len(settlements[settlement_row]) - 1 - col])

# Fill in reverse for negative-slope dividing line
right_arr_neg.append(settlements[bottom_settlement_row][col])
left_arr_neg.append(settlements[settlement_row][len(settlements[settlement_row]) - 1 - col])


# Calculates the resource distribution score across the two provided groups (lists) of SettlementLocations.
# This is done in the following method:
# 1) Sum the frequency of each available resource across each group (using available_resource_distribution)
# 2) Calculate the difference between each group, for each ResourceType
# 3) Square each resulting difference
# 4) Sum the squares, the result being returned as the final result
def calculate_resource_distribution_score(group_one, group_two):
# Calculate the group resource sums of both groups (1)
group_one_sums = calculate_group_resource_sums(group_one)
group_two_sums = calculate_group_resource_sums(group_two)

# Calculate and square the difference between the two groups (2 & 3)
diffs_squared = {k: (group_one_sums.get(k, 0) - group_two_sums.get(k, 0)) ** 2 for k in set(group_one_sums)}

# Calculate the sum of the squared differences and return it
sum_of_squares = 0

for i in range(1, 6):
sum_of_squares += diffs_squared[i]

return sum_of_squares


# Generates a dictionary with keys corresponding to ResourceType.values and values corresponding to how the sum of the
# available resource frequencies of each SettlementLocation in the group (group = a list of SettlementLocations)
def calculate_group_resource_sums(group):
# Generate dictionary to keep track of the sums of the frequencies of each ResourceType (key = ResourceType.value)
group_sums = {k: 0 for k in list(range(1, 6))}

# Now iterate over each group and update the group sums accordingly
for settlement_location in group:
resource_frequency_count = settlement_location.available_resource_distribution

# These dictionaries have the same keys, thus we can use a one-liner to combine them via addition
group_sums = {k: group_sums.get(k, 0) + resource_frequency_count.get(k, 0) for k in
set(resource_frequency_count)}

return group_sums
129 changes: 129 additions & 0 deletions catan/balance/resource_distribution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import math


# Divides the island into equal parts and analyzes the frequency of available resources, three times
def measure_resource_distribution(board):
# For convenience
settlements = board.settlement_locations
# Calculate for horizontal division (horizontal line through the center of the board) - split into two groups
top_half = []
bottom_half = []

for row in range(math.ceil(len(settlements) / 2)):
top_row = settlements[row]
bottom_row = settlements[len(settlements) - 1 - row]

# Due to the symmetric nature of a Catan board, top_row and bottom_row will have the same lengths
for index in range(len(top_row)):
top_half.append(top_row[index])
bottom_half.append(bottom_row[index])

# Calculate the resource distribution score for this horizontal division
horizontal_division_score = calculate_resource_distribution_score(top_half, bottom_half)

# Calculate for the positive-slope diagonal division (from bottom left to top right)
left_half_positive = []
right_half_positive = []

# And also for the negative-slope diagonal division (from top left to bottom right)
left_half_negative = []
right_half_negative = []

# Populate these arrays
# This is essentially a row-by-row operation, as there is not a solid coordinate system to work with

# A dictionary mapping each index in settlements to how many SettlementLocations lie to the right of the
# positive-slope dividing line or to the left of the negative-slope dividing line in the TOP HALF of the board.
# This will be used to call fill_arrays_from_sides and populate the left and right half arrays.
# Only indices 0-5 are mapped because the vertical symmetry is utilized to manage the other half
row_to_num_on_side = {
0: 0,
1: 1,
2: 1,
3: 2,
4: 2,
5: 3
}

# Use the items in the dictionary to call fill_arrays_from_sides
for settlement_row, num_right in row_to_num_on_side.items():
fill_arrays_from_sides(left_half_positive, right_half_positive, left_half_negative, right_half_negative,
settlements, settlement_row, num_right)

# Calculate the resource distribution score for these divisions
positive_slope_division_score = calculate_resource_distribution_score(left_half_positive, right_half_positive)
negative_slope_division_score = calculate_resource_distribution_score(left_half_negative, right_half_negative)

# Calculate and return the final result by summing the scores for each of the 3 divisions
return horizontal_division_score + positive_slope_division_score + negative_slope_division_score


# Adds SettlementLocations from settlements (A Board's settlement_locations) in the row in the TOP HALF of the board,
# settlement_row, to the left and right-side groups for the positive-slope diving line, left_arr_pos and right_arr_pos,
# of SettlementLocations. num_on_side is an integer, indicating how many SettlementLocations on this row (in the TOP
# HALF) are on the right side of the positive-slope line or on the left side of the negative-slope line. This method
# utilizes the vertical symmetry of the board to also add the SettlementLocations from the row vertically opposite to
# settlement_row.
def fill_arrays_from_sides(left_arr_pos, right_arr_pos, left_arr_neg, right_arr_neg, settlements,
settlement_row, num_on_side):
bottom_settlement_row = len(settlements) - 1 - settlement_row

for col in range(len(settlements[settlement_row]) - num_on_side):
bottom_col = len(settlements[bottom_settlement_row]) - 1 - col

# Fill for positive-slope dividing line
left_arr_pos.append(settlements[settlement_row][col])
right_arr_pos.append(settlements[bottom_settlement_row][bottom_col])

# Fill in reverse for negative-slope dividing line
right_arr_neg.append(settlements[settlement_row][col])
left_arr_neg.append(settlements[bottom_settlement_row][bottom_col])

for col in range(num_on_side):
# Fill for positive-slope dividing line
left_arr_pos.append(settlements[bottom_settlement_row][col])
right_arr_pos.append(settlements[settlement_row][len(settlements[settlement_row]) - 1 - col])

# Fill in reverse for negative-slope dividing line
right_arr_neg.append(settlements[bottom_settlement_row][col])
left_arr_neg.append(settlements[settlement_row][len(settlements[settlement_row]) - 1 - col])


# Calculates the resource distribution score across the two provided groups (lists) of SettlementLocations.
# This is done in the following method:
# 1) Sum the frequency of each available resource across each group (using available_resource_distribution)
# 2) Calculate the difference between each group, for each ResourceType
# 3) Square each resulting difference
# 4) Sum the squares, the result being returned as the final result
def calculate_resource_distribution_score(group_one, group_two):
# Calculate the group resource sums of both groups (1)
group_one_sums = calculate_group_resource_sums(group_one)
group_two_sums = calculate_group_resource_sums(group_two)

# Calculate and square the difference between the two groups (2 & 3)
diffs_squared = {k: (group_one_sums.get(k, 0) - group_two_sums.get(k, 0)) ** 2 for k in set(group_one_sums)}

# Calculate the sum of the squared differences and return it
sum_of_squares = 0

for i in range(1, 6):
sum_of_squares += diffs_squared[i]

return sum_of_squares


# Generates a dictionary with keys corresponding to ResourceType.values and values corresponding to how the sum of the
# available resource frequencies of each SettlementLocation in the group (group = a list of SettlementLocations)
def calculate_group_resource_sums(group):
# Generate dictionary to keep track of the sums of the frequencies of each ResourceType (key = ResourceType.value)
group_sums = {k: 0 for k in list(range(1, 6))}

# Now iterate over each group and update the group sums accordingly
for settlement_location in group:
resource_frequency_count = settlement_location.available_resource_distribution

# These dictionaries have the same keys, thus we can use a one-liner to combine them via addition
group_sums = {k: group_sums.get(k, 0) + resource_frequency_count.get(k, 0) for k in
set(resource_frequency_count)}

return group_sums
6 changes: 3 additions & 3 deletions catan/board/board.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .number_tokens import NumberToken
from .settlement_location import SettlementLocation
from .terrain_hexes import TerrainHex, TerrainType
from catan.board.number_tokens import NumberToken
from catan.board.settlement_location import SettlementLocation
from catan.board.terrain_hexes import TerrainHex, TerrainType


# A class representing and containing the methods of a Catan board
Expand Down
3 changes: 2 additions & 1 deletion tests/test_balance_functions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from catan.balance.balance_functions import measure_resource_distribution
from catan.balance.resource_distribution import measure_resource_distribution
from catan.board.board import Board
from catan.board.known_layouts import perfectly_distributed_layout


def test_measure_resource_distribution():
# Assert that the measured resource distribution of the perfectly distributed layout is 0
assert measure_resource_distribution(Board(terrain_types=perfectly_distributed_layout['terrain_types'],
tile_numbers=perfectly_distributed_layout['tile_numbers'])) == 0

0 comments on commit e27edfb

Please sign in to comment.