sehsapneb commited on
Commit
234472d
·
verified ·
1 Parent(s): 74594d1

Create ai.py

Browse files
Files changed (1) hide show
  1. ai.py +129 -0
ai.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ai.py
2
+ import numpy as np
3
+ from game_logic import (
4
+ get_possible_moves, get_empty_cells, evaluate_board,
5
+ is_game_over, DIRECTIONS
6
+ )
7
+ import time
8
+
9
+ # Cache: key=board_tuple, value=score
10
+ # Use tuple(board.flatten()) as key because arrays are not hashable
11
+ cache = {}
12
+ SEARCH_DEPTH = 4 # <<<===== START WITH 3 or 4. 6 WILL BE VERY SLOW/TIMEOUT!
13
+ MAX_CACHE_SIZE = 50000 # Prevent memory explosion
14
+
15
+ def clear_cache():
16
+ global cache
17
+ # Simple cache clearing if it gets too big
18
+ if len(cache) > MAX_CACHE_SIZE:
19
+ print(f"Clearing cache ({len(cache)})")
20
+ cache = {}
21
+ # Or just clear every top-level call:
22
+ # cache = {}
23
+
24
+
25
+ # Expectimax: alternates between player (max) and chance (avg) nodes
26
+ def expectimax(board: np.ndarray, depth: int, is_player_turn: bool) -> float:
27
+ """
28
+ Recursive Expectimax search.
29
+ Returns the heuristic score for the given board state and depth.
30
+ """
31
+ board_tuple = tuple(board.flatten())
32
+ # Cache lookup: include depth to ensure we don't reuse a shallow search result for a deep one
33
+ cache_key = (board_tuple, depth, is_player_turn)
34
+ if cache_key in cache:
35
+ return cache[cache_key]
36
+
37
+ # Base Cases
38
+ if depth == 0 or is_game_over(board):
39
+ return evaluate_board(board)
40
+
41
+ # --- Player's Turn (Maximize) ---
42
+ if is_player_turn:
43
+ best_score = -float('inf')
44
+ possible_moves = get_possible_moves(board)
45
+ if not possible_moves:
46
+ return evaluate_board(board) # Game over / stuck
47
+
48
+ for move_info in possible_moves:
49
+ # Recursively call for chance node after player move
50
+ score = expectimax(move_info['board'], depth - 1, False)
51
+ # Add score gained directly by this move? evaluate_board can also include it.
52
+ # score += move_info['score']
53
+ if score > best_score:
54
+ best_score = score
55
+ cache[cache_key] = best_score
56
+ return best_score
57
+
58
+ # --- Chance Node's Turn (Average / Expectation) ---
59
+ else:
60
+ avg_score = 0.0
61
+ empty_cells = get_empty_cells(board)
62
+ if not empty_cells:
63
+ # This state should technically not be reached if the player move was valid,
64
+ # but handle defensively.
65
+ return evaluate_board(board)
66
+
67
+ # Calculate expected score over all possible random tile placements
68
+ prob_2 = 0.9 / len(empty_cells)
69
+ prob_4 = 0.1 / len(empty_cells)
70
+
71
+ for r, c in empty_cells:
72
+ # Case 1: Add a '2' tile
73
+ board[r, c] = 2
74
+ avg_score += prob_2 * expectimax(board, depth - 1, True) # Player's turn next
75
+
76
+ # Case 2: Add a '4' tile
77
+ board[r, c] = 4
78
+ avg_score += prob_4 * expectimax(board, depth - 1, True) # Player's turn next
79
+
80
+ board[r, c] = 0 # backtrack / reset cell for next iteration
81
+
82
+ cache[cache_key] = avg_score
83
+ return avg_score
84
+
85
+ def find_best_move(board: np.ndarray, depth: int) -> str:
86
+ """
87
+ Entry point for the AI. Finds the best move from the current board.
88
+ """
89
+ start_time = time.time()
90
+ clear_cache() # Clear cache for each new top-level request
91
+
92
+ best_score = -float('inf')
93
+ best_move = 'g' # Default to game over
94
+
95
+ possible_moves = get_possible_moves(board)
96
+
97
+ if not possible_moves:
98
+ print("AI: No possible moves.")
99
+ return 'g' # Game over
100
+
101
+ # Evaluate each possible first move
102
+ # Note: depth here applies to the search *after* this first move
103
+ for move_info in possible_moves:
104
+ # Start recursion: after player move, it's the chance node's turn
105
+ # Use depth, not depth-1, because this is the root
106
+ score = expectimax(move_info['board'], depth , False)
107
+ # score += move_info['score'] # Optional: add immediate score gain
108
+ print(f" Move {move_info['dir']}: Score {score:.2f}")
109
+ if score > best_score:
110
+ best_score = score
111
+ best_move = move_info['dir']
112
+
113
+ # Fallback if all moves lead to terrible scores, just pick the first valid one
114
+ if best_move == 'g' and possible_moves:
115
+ best_move = possible_moves[0]['dir']
116
+
117
+ duration = time.time() - start_time
118
+ print(f"AI Decision: {best_move} (Score: {best_score:.2f}, Depth: {depth}, Time: {duration:.4f}s, Cache: {len(cache)})")
119
+ return best_move
120
+
121
+ # Adjust depth based on board state (optional but good)
122
+ def get_dynamic_depth(board):
123
+ empty = len(get_empty_cells(board))
124
+ # Use deeper search when fewer choices, shallower when many choices
125
+ if empty <= 4:
126
+ return SEARCH_DEPTH + 1
127
+ #if empty >= 10:
128
+ # return max(2, SEARCH_DEPTH -1)
129
+ return SEARCH_DEPTH