diff --git a/algorithm/__init__.py b/algorithm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/algorithm/feature_context.py b/algorithm/feature_context.py index 1c3c53e..317fdb9 100644 --- a/algorithm/feature_context.py +++ b/algorithm/feature_context.py @@ -2,19 +2,24 @@ from numpy.typing import NDArray from dataclasses import dataclass from game.board import compute_prefix_sum +from game.action import Action +from game.board import Board @dataclass class FeatureContext: - board: NDArray[np.int8] # 숫자 배열의 좌푯값 - prefix: NDArray # 누적합 - count_by_value: dict[int, int] #숫자별 남아있는 개수 + board: NDArray[np.int8] + prefix: NDArray + count_by_value: dict[int, int] + valid_actions: list[Action] @classmethod - def from_board(cls, board: NDArray[np.int8]) -> "FeatureContext": - prefix = compute_prefix_sum(board) - counts = {v: int((board == v).sum()) for v in range(1, 10)} - return cls(board=board, prefix = prefix, count_by_value = counts) + def from_board(cls, board_obj: Board) -> "FeatureContext": + grid = board_obj.grid # Board 객체 안의 실제 배열 + prefix = compute_prefix_sum(grid) #누적합 + counts = {v: int((grid == v).sum()) for v in range(1, 10)} + valid_actions = board_obj.get_valid_actions() + return cls(board=grid, prefix=prefix, count_by_value=counts, valid_actions=valid_actions) diff --git a/algorithm/features.py b/algorithm/features.py index 577e3a4..be5e03e 100644 --- a/algorithm/features.py +++ b/algorithm/features.py @@ -1,8 +1,127 @@ -from .feature_context import feature_context +from .feature_context import FeatureContext +from .math_algorithm import sigmoid +from game.action import get_cleared_cells -def feature_remove_nine(ctx: feature_context) -> float: - return +#숫자 편향 features ex) 지울 수 있는 'n'이라는 숫자가 몇 개 남았는가 점수를 올림으로써 지우게끔 유도 +def feature_remove_nine(ctx: FeatureContext) -> float: + area = ctx.board + if not(area == 9).any(): #격자 안에 9가 없을시 0을 return한다 + return 0.0 + nine_count = int((area==9).sum()) #area안에 9가 있는 수의 개수 + if not _has_nine_one_pair(ctx): #9와 짝 지어지는 경우의 수가 판 내에 존재하는지 + return 0.0 + return sigmoid(nine_count, k = 0.5, x0 = 3.0) #추후 이 값을 조정하면서 k 값과 x0 값을 찾아도 됨 + +def feature_remove_eight(ctx: FeatureContext) -> float: + area = ctx.board + if not(area == 8).any(): #격자 안에 9가 없을시 0을 return한다 + return 0.0 + eight_count = int((area==8).sum()) #area안에 9가 있는 수의 개수 + if not _has_eight_pair(ctx): #9와 짝 지어지는 경우의 수가 판 내에 존재하는지 + return 0.0 + return sigmoid(eight_count, k = 0.5, x0 = 3.0) #추후 이 값을 조정하면서 k 값과 x0 값을 찾아도 됨 + + +def feature_remove_seven(ctx: FeatureContext) -> float: + area = ctx.board + if not(area == 7).any(): #격자 안에 9가 없을시 0을 return한다 + return 0.0 + seven_count = int((area==7).sum()) #area안에 9가 있는 수의 개수 + if not _has_seven_pair(ctx): #9와 짝 지어지는 경우의 수가 판 내에 존재하는지 + return 0.0 + return sigmoid(seven_count, k = 0.5, x0 = 3.0) #추후 이 값을 조정하면서 k 값과 x0 값을 찾아도 됨 + +def feature_remove_six(ctx: FeatureContext) -> float: + area = ctx.board + if not(area == 6).any(): #격자 안에 9가 없을시 0을 return한다 + return 0.0 + six_count = int((area==6).sum()) #area안에 9가 있는 수의 개수 + if not _has_six_pair(ctx): #9와 짝 지어지는 경우의 수가 판 내에 존재하는지 + return 0.0 + return sigmoid(six_count, k = 0.5, x0 = 3.0) #추후 이 값을 조정하면서 k 값과 x0 값을 찾아도 됨 + +def feature_remove_five(ctx: FeatureContext) -> float: + area = ctx.board + if not(area == 5).any(): #격자 안에 9가 없을시 0을 return한다 + return 0.0 + five_count = int((area==5).sum()) #area안에 9가 있는 수의 개수 + if not _has_five_pair(ctx): #9와 짝 지어지는 경우의 수가 판 내에 존재하는지 + return 0.0 + return sigmoid(five_count, k = 0.5, x0 = 3.0) #추후 이 값을 조정하면서 k 값과 x0 값을 찾아도 됨 + +def feature_edge_bias(board, action): + """action으로 지워지는 셀들이 가장자리에 가까울수록 높은 값 (0~1)""" + cells = get_cleared_cells(board, action) + rows, cols = board.rows, board.cols + scores = [] + for r, c in cells: + dist_to_edge = min(r, rows - 1 - r, c, cols - 1 - c) + max_dist = min(rows, cols) // 2 + scores.append(1 - dist_to_edge / max_dist) + return sum(scores) / len(scores) + + +def feature_center_bias(board, action): + """action으로 지워지는 셀들이 중앙에 가까울수록 높은 값 (0~1)""" + cells = get_cleared_cells(board, action) + rows, cols = board.rows, board.cols + center_r, center_c = (rows - 1) / 2, (cols - 1) / 2 + max_dist = (center_r ** 2 + center_c ** 2) ** 0.5 + scores = [] + for r, c in cells: + dist_to_center = ((r - center_r) ** 2 + (c - center_c) ** 2) ** 0.5 + scores.append(1 - dist_to_center / max_dist) + return sum(scores) / len(scores) + +#feature_remove series를 구현하기 위한 보조 함수 +#9와 짝이 되는 valid action이 있는지 여부 확인 +def _has_nine_one_pair(ctx : FeatureContext) -> bool: + + for action in ctx.valid_actions: + r1, c1 = action.top_left + r2, c2 = action.bottom_right + region = ctx.board[r1:r2+1, c1:c2+1] + if (region == 9).any(): + return True + return False +def _has_eight_pair(ctx : FeatureContext) -> bool: + + for action in ctx.valid_actions: + r1, c1 = action.top_left + r2, c2 = action.bottom_right + region = ctx.board[r1:r2+1, c1:c2+1] + if (region == 8).any(): + return True + return False +def _has_seven_pair(ctx : FeatureContext) -> bool: + + for action in ctx.valid_actions: + r1, c1 = action.top_left + r2, c2 = action.bottom_right + region = ctx.board[r1:r2+1, c1:c2+1] + if (region == 7).any(): + return True + return False +def _has_six_pair(ctx : FeatureContext) -> bool: + + for action in ctx.valid_actions: + r1, c1 = action.top_left + r2, c2 = action.bottom_right + region = ctx.board[r1:r2+1, c1:c2+1] + if (region == 6).any(): + return True + return False +def _has_five_pair(ctx : FeatureContext) -> bool: + + for action in ctx.valid_actions: + r1, c1 = action.top_left + r2, c2 = action.bottom_right + region = ctx.board[r1:r2+1, c1:c2+1] + if (region == 5).any(): + return True + return False + diff --git a/algorithm/math_algorithm.py b/algorithm/math_algorithm.py new file mode 100644 index 0000000..57ac3e6 --- /dev/null +++ b/algorithm/math_algorithm.py @@ -0,0 +1,4 @@ +import math + +def sigmoid(x: float, k: float = 0.5, x0: float = 3.0) -> float: + return 1 / (1 + math.exp(-k * (x - x0))) \ No newline at end of file diff --git a/algorithm/simulator.py b/algorithm/simulator.py index 130f3f4..6a15322 100644 --- a/algorithm/simulator.py +++ b/algorithm/simulator.py @@ -27,7 +27,7 @@ def play_game(self, board: Board) -> GameResult: break actions = board.get_valid_actions() - best_action = pick_best_action(actions, board.grid, self.weights) + best_action = pick_best_action(actions, board.grid, self.weights) area = self.get_area(board.grid, best_action) cleared = int((area != 0).sum()) # 이번에 지운 칸 수 diff --git a/game/action.py b/game/action.py index f0bbc7e..2967577 100644 --- a/game/action.py +++ b/game/action.py @@ -4,4 +4,12 @@ class Action: # (row, col) top_left: tuple[int, int] - bottom_right: tuple[int, int] \ No newline at end of file + bottom_right: tuple[int, int] +def get_cleared_cells(self, board) -> list[tuple[int, int]]: + """이 action으로 지워지는(0이 아닌) 칸들의 좌표 리스트 반환""" + (r1, c1), (r2, c2) = self.top_left, self.bottom_right + area = board[r1:r2+1, c1:c2+1] + return [(r1 + i, c1 + j) + for i in range(area.shape[0]) + for j in range(area.shape[1]) + if area[i, j] != 0] \ No newline at end of file