| |
| import numpy as np |
| import random |
| import math |
|
|
| |
| def parse_board_hex(hex_string: str) -> np.ndarray: |
| """Converts 16-char hex string (exponents) to 4x4 numpy array (values).""" |
| if len(hex_string) != 16: |
| raise ValueError("Board string must be 16 characters long") |
| board = np.zeros((4, 4), dtype=int) |
| for i, char in enumerate(hex_string): |
| exponent = int(char, 16) |
| value = 0 |
| if exponent > 0: |
| |
| value = 1 << exponent |
| row, col = divmod(i, 4) |
| board[row, col] = value |
| return board |
| |
| def get_empty_cells(board: np.ndarray): |
| """Returns a list of (row, col) tuples for empty cells.""" |
| return list(zip(*np.where(board == 0))) |
|
|
| |
| |
|
|
| def _compress(row): |
| """Move all non-zero tiles to the left.""" |
| new_row = [i for i in row if i != 0] |
| new_row.extend([0] * (4 - len(new_row))) |
| return new_row |
|
|
| def _merge(row): |
| """Merge identical adjacent tiles (left to right), returns new row and score gained.""" |
| score = 0 |
| new_row = list(row) |
| for i in range(3): |
| if new_row[i] == new_row[i+1] and new_row[i] != 0: |
| new_row[i] *= 2 |
| score += new_row[i] |
| new_row[i+1] = 0 |
| return new_row, score |
|
|
| def move_left(board: np.ndarray): |
| """Executes a left move, returns (new_board, changed, score_gained).""" |
| new_board = np.zeros_like(board) |
| total_score = 0 |
| changed = False |
| for i in range(4): |
| row = board[i] |
| compressed_row = _compress(row) |
| merged_row, score = _merge(compressed_row) |
| final_row = _compress(merged_row) |
| new_board[i] = final_row |
| total_score += score |
| if not np.array_equal(row, final_row): |
| changed = True |
| return new_board, changed, total_score |
|
|
| |
| def _transpose(board): |
| return np.transpose(board) |
| |
| def _reverse_rows(board): |
| return np.array([row[::-1] for row in board]) |
|
|
| def move_right(board: np.ndarray): |
| reversed_board = _reverse_rows(board) |
| new_board, changed, score = move_left(reversed_board) |
| return _reverse_rows(new_board), changed, score |
|
|
| def move_up(board: np.ndarray): |
| transposed_board = _transpose(board) |
| new_board, changed, score = move_left(transposed_board) |
| return _transpose(new_board), changed, score |
|
|
| def move_down(board: np.ndarray): |
| transposed_board = _transpose(board) |
| new_board, changed, score = move_right(transposed_board) |
| return _transpose(new_board), changed, score |
|
|
| MOVE_MAP = { |
| 'u': move_up, |
| 'd': move_down, |
| 'l': move_left, |
| 'r': move_right, |
| } |
| DIRECTIONS = ['u', 'd', 'l', 'r'] |
|
|
| def get_possible_moves(board: np.ndarray): |
| """Returns a list of (direction_char, new_board, score) for all VALID moves.""" |
| possible = [] |
| for direction in DIRECTIONS: |
| move_func = MOVE_MAP[direction] |
| new_board, changed, score = move_func(board) |
| if changed: |
| possible.append({'dir': direction, 'board': new_board, 'score': score}) |
| return possible |
| |
| def is_game_over(board: np.ndarray): |
| """Check if any move is possible.""" |
| if get_empty_cells(board): |
| return False |
| |
| for move_func in MOVE_MAP.values(): |
| _, changed, _ = move_func(board) |
| if changed: |
| return False |
| return True |
|
|
| |
| |
| |
|
|
| def evaluate_board(board: np.ndarray, score_gained:int=0) -> float: |
| """ Assign a score to a board state. Higher is better.""" |
| empty_cells = len(get_empty_cells(board)) |
| if empty_cells == 0 and is_game_over(board): |
| return -100000.0 |
|
|
| |
| |
| |
| |
| |
| |
| smoothness_penalty = 0 |
| for r in range(4): |
| for c in range(4): |
| if board[r,c] > 0: |
| log_val = math.log2(board[r,c]) |
| |
| if c + 1 < 4 and board[r, c+1] > 0: |
| smoothness_penalty += abs(log_val - math.log2(board[r, c+1])) |
| |
| if r + 1 < 4 and board[r+1, c] > 0: |
| smoothness_penalty += abs(log_val - math.log2(board[r+1, c])) |
|
|
| |
| |
| mono_penalty = 0 |
| |
| for c in range(4): |
| for r in range(3): |
| if board[r,c] > 0 and board[r+1,c] > 0 and board[r+1,c] > board[r,c] : |
| mono_penalty += math.log2(board[r+1,c]) - math.log2(board[r,c]) |
| |
| |
| for r in range(4): |
| for c in range(3): |
| if board[r,c] > 0 and board[r,c+1] > 0 and board[r, c+1] > board[r,c]: |
| mono_penalty += math.log2(board[r,c+1]) - math.log2(board[r,c]) |
| |
|
|
| |
| max_tile_bonus = math.log2(board.max()) if board.max() > 0 else 0 |
| |
| |
| EMPTY_WEIGHT = 200.0 |
| SMOOTH_WEIGHT = 0.5 |
| MONO_WEIGHT = 1.5 |
| MAX_TILE_WEIGHT = 1.0 |
| |
| |
| |
| if board.max() == 0: return 0.0 |
|
|
| heuristic_score = ( |
| empty_cells * EMPTY_WEIGHT |
| |
| + max_tile_bonus * MAX_TILE_WEIGHT |
| - smoothness_penalty * SMOOTH_WEIGHT |
| - mono_penalty * MONO_WEIGHT |
| ) |
| |
| return heuristic_score |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|