-
Notifications
You must be signed in to change notification settings - Fork 0
/
cases.py
639 lines (504 loc) · 22.3 KB
/
cases.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# test_cases_1.py (repeat this structure for test_cases_2.py, test_cases_3.py, and test_cases_4.py)
from PushBattle import Game, PLAYER1, PLAYER2, EMPTY, BOARD_SIZE, NUM_PIECES, _torus
import numpy as np
import time
import logging
class RandomAgent:
def __init__(self, player=PLAYER1):
self.player = player
# given the game state, gets all of the possible moves
def get_possible_moves(self, game):
"""Returns list of all possible moves in current state."""
moves = []
current_pieces = game.p1_pieces if game.current_player == PLAYER1 else game.p2_pieces
if current_pieces < NUM_PIECES:
# placement moves
for r in range(BOARD_SIZE):
for c in range(BOARD_SIZE):
if game.board[r][c] == EMPTY:
moves.append((r, c))
else:
# movement moves
for r0 in range(BOARD_SIZE):
for c0 in range(BOARD_SIZE):
if game.board[r0][c0] == game.current_player:
for r1 in range(BOARD_SIZE):
for c1 in range(BOARD_SIZE):
if game.board[r1][c1] == EMPTY:
moves.append((r0, c0, r1, c1))
return moves
def get_best_move(self, game):
"""Returns a random valid move."""
possible_moves = self.get_possible_moves(game)
return random.choice(possible_moves)
# Set up logging
logging.basicConfig(filename='test_cases_1.log',
level=logging.INFO, format='%(asctime)s - %(message)s')
def evaluate_runtime(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
elapsed_time = end_time - start_time
logging.info(
f"{func.__name__} - Elapsed time: {elapsed_time:.6f} seconds")
return result
return wrapper
# example function for static checks
@evaluate_runtime
def static_weight(game,
board,
turn_count,
attempt_number,
lastFitness):
# useful variables
current_player = game.current_player
p1_tiles = game.p1_tiles
p2_tiles = game.p2_tiles
output = np.zeros((8, 8))
# add your code here
return output
def get_hot(heatmap):
# Ensure the input is a NumPy array
heatmap = np.array(heatmap)
# Check if the input is an 8x8 array
if heatmap.shape != (8, 8):
raise ValueError("Input heatmap must be an 8x8 array")
# Flatten the array and get the indices of the top 7 values
flat_indices = np.argpartition(heatmap.ravel(), -7)[-7:]
# Sort the indices based on their values in descending order
sorted_indices = flat_indices[np.argsort(
heatmap.ravel()[flat_indices])[::-1]]
# Convert flat indices to 2D coordinates
top_7_locations = [(index // 8, index % 8) for index in sorted_indices]
return top_7_locations
def get_coldest(heatmap, game):
"""
Find the coldest cell that contains an ally piece.
Parameters:
heatmap (np.array): 8x8 numpy array representing the heatmap
game (Game): The current game state
Returns:
tuple: (y, x) coordinates of the coldest cell with an ally piece, or None if no such cell exists
"""
# Ensure the heatmap is a NumPy array
heatmap = np.array(heatmap)
# Check if the input is an 8x8 array
if heatmap.shape != (BOARD_SIZE, BOARD_SIZE):
raise ValueError("Input heatmap must be an 8x8 array")
# Create a mask for ally pieces
ally_mask = game.board == game.current_player
# Apply the mask to the heatmap
masked_heatmap = np.where(ally_mask, heatmap, np.inf)
# Find the minimum value in the masked heatmap
min_value = np.min(masked_heatmap)
# If there are no ally pieces, return None
if np.isinf(min_value):
return None
# Find the coordinates of the minimum value
y, x = np.unravel_index(np.argmin(masked_heatmap), masked_heatmap.shape)
return (y, x)
def get_possible_heatmap_moves(game, coldest_cell, target_cells):
"""
Get all possible moves that either PLACE into or MOVE into the given target cells.
Parameters:
game (Game): The current game state
coldest_cell (tuple): (y, x) coordinates of the cell to move from (for MOVE actions)
target_cells (list): List of (y, x) coordinates of target cells to move to or place in
Returns:
list: List of valid moves (each move is a list of integers)
"""
possible_moves = []
current_pieces = game.p1_pieces if game.current_player == PLAYER1 else game.p2_pieces
# If we haven't placed all pieces, consider PLACE actions
if current_pieces < NUM_PIECES:
for y, x in target_cells:
if game.is_valid_placement(y, x):
possible_moves.append([y, x]) # PLACE move
# Consider MOVE actions
if current_pieces == NUM_PIECES:
cy, cx = coldest_cell
if game.board[cy][cx] == game.current_player:
for y, x in target_cells:
if game.is_valid_move(cy, cx, y, x):
possible_moves.append([cy, cx, y, x]) # MOVE move
return possible_moves
###### FITNESS STUFF ######
@evaluate_runtime
def evaluate_piece_count(current_player_pieces):
return current_player_pieces
@evaluate_runtime
def evaluate_defensive_formation(board, player):
defensive_score = 0
directions = [(0, 1), (1, 0), (1, 1), (1, -1),
(-1, 0), (0, -1), (-1, -1), (-1, 1)]
for r in range(8):
for c in range(8):
if board[r][c] == player:
backed_up = 0
for dr, dc in directions:
nr = (r + dr) % 8 # Wrap around rows
nc = (c + dc) % 8 # Wrap around columns
if board[nr][nc] == player:
backed_up += 1
if backed_up >= 2:
defensive_score += 3.0
if backed_up >= 3:
defensive_score += 2.0 # Additional bonus for extra support
return defensive_score * 2.5 # Applying the weight
@evaluate_runtime
def evaluate_distance_to_victory(board, player, opponent):
victory_score = 0
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
size = len(board) # Assuming the board is square (8x8)
def check_line(line, player):
player_count = line.count(player)
empty_count = line.count(None)
if player_count + empty_count == 3:
if player_count == 2:
return 10 # One move away from victory
elif player_count == 1:
return 5 # Two moves away from victory
elif player_count == 0:
return 1 # Three moves away (potential future line)
return 0
for r in range(size):
for c in range(size):
for dr, dc in directions:
line = []
for i in range(3):
nr = (r + i*dr) % size # Wrap around rows
nc = (c + i*dc) % size # Wrap around columns
line.append(board[nr][nc])
if len(line) == 3:
victory_score += check_line(line, player)
# Slightly less weight for opponent's threats
victory_score -= check_line(line, opponent) * 0.8
return victory_score
@evaluate_runtime
def evaluate_opponent_cluster_disruption(board, opponent):
disruption_score = 0
size = len(board)
directions = [(0, 1), (1, 0), (1, 1), (1, -1),
(0, -1), (-1, 0), (-1, -1), (-1, 1)]
def is_opponent(r, c):
return board[r][c] == opponent
def get_cluster_size(r, c):
visited = set()
stack = [(r, c)]
cluster_size = 0
while stack:
current_r, current_c = stack.pop()
if (current_r, current_c) not in visited:
visited.add((current_r, current_c))
cluster_size += 1
for dr, dc in directions:
nr = (current_r + dr) % size
nc = (current_c + dc) % size
if is_opponent(nr, nc) and (nr, nc) not in visited:
stack.append((nr, nc))
return cluster_size
# Explore the board for potential moves that disrupt opponent clusters
for r in range(size):
for c in range(size):
if board[r][c] is None: # Empty cell where we can potentially move
for dr, dc in directions:
nr = (r + dr) % size
nc = (c + dc) % size
if is_opponent(nr, nc):
original_cluster_size = get_cluster_size(nr, nc)
if original_cluster_size >= 2:
# Simulate pushing the opponent piece
push_r = (nr + dr) % size
push_c = (nc + dc) % size
if board[push_r][push_c] is None: # Can push
# Temporarily move the piece
board[nr][nc], board[push_r][push_c] = None, opponent
new_cluster_size = get_cluster_size(
push_r, push_c)
# Revert the move
board[nr][nc], board[push_r][push_c] = opponent, None
if new_cluster_size < original_cluster_size:
disruption_score += 4 # Apply weight for disrupting clusters
return disruption_score
@evaluate_runtime
def evaluate_potential_win_pathways(board, player):
pathway_score = 0
size = len(board) # Assuming a square board
# Horizontal, vertical, diagonal
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
def is_player_piece(r, c):
return board[r][c] == player
def is_empty(r, c):
return board[r][c] is None
# Check all empty spaces for potential win pathways
for r in range(size):
for c in range(size):
if is_empty(r, c): # Only check empty cells
for dr, dc in directions:
count = 0
empty_count = 0
# Check in both directions
for i in range(-2, 3): # Check 2 spaces before and after
if i == 0:
continue # Skip the current cell
nr = (r + i * dr) % size # Wrap around rows
nc = (c + i * dc) % size # Wrap around columns
if is_player_piece(nr, nc):
count += 1
elif is_empty(nr, nc):
empty_count += 1
else:
break # Blocked by opponent's piece
# Evaluate the potential pathway
if count + empty_count >= 2:
if count == 2:
pathway_score += 3 # Higher score for two pieces already aligned
elif count == 1:
pathway_score += 2 # Standard score for one piece in the pathway
else:
pathway_score += 1 # Small score for potential future alignment
return pathway_score
def calculate_board_score(board, player):
EMPTY = 0 # Assume EMPTY is represented as 0
enemy = 3 - player # Assuming players are represented as 1 and 2
# horizontal, vertical, two diagonals
directions = [(0, 1), (1, 0), (1, 1), (1, -1)]
fitness = 0
def check_line(y, x, dy, dx, length):
line = [board[(y + i*dy) % len(board)][(x + i*dx) %
len(board)] for i in range(length)]
return line
for y in range(len(board)):
for x in range(len(board)):
if board[y][x] == EMPTY:
for dy, dx in directions:
# Check for ally conditions
line = check_line(y, x, dy, dx, 5)
if line[2] == EMPTY:
if line[1] == player and line[3] == player:
if line[0] == EMPTY and line[4] == EMPTY:
fitness += 10 # Condition 1: Two ally tiles with open spaces on both sides
else:
fitness += 7 # Condition 5: Two ally tiles with a space between
elif line.count(player) >= 2 and line[0] == EMPTY and line[4] == EMPTY:
fitness += 10 # Condition 1: Two or more ally tiles with open spaces on both sides
# Check for enemy conditions
if line.count(enemy) >= 2:
if line[0] == EMPTY and line[4] == EMPTY:
fitness += 10 # Condition 2: Two or more enemy tiles with open spaces
elif line[2] == EMPTY and line[1] == enemy and line[3] == enemy:
fitness = 0 # Condition 3: Two enemy tiles with a space between
# Check for 3 in a row
if line[1:4].count(player) == 3:
fitness += 20 # Condition 4: 3 in a row for ally
elif board[y][x] != EMPTY:
fitness = 0 # Condition 6: Space is already occupied
return fitness
@evaluate_runtime
def get_best_move(board, player):
best_score = float('-inf')
best_move = None
for y in range(len(board)):
for x in range(len(board)):
if board[y][x] == EMPTY:
board[y][x] = player
score = calculate_board_score(board, player)
board[y][x] = EMPTY
if score > best_score:
best_score = score
best_move = (y, x)
return best_move, best_score
def get_fitness(game,
turn_count,
move): # move is (y, x)
# create the new game state
game_copy = Game.from_dict(game.to_dict())
if len(move) == 2:
game_copy.place_checker(move[0], move[1])
else:
game_copy.move_checker(move[0], move[1], move[2], move[3])
# useful variables
board = game_copy.board
p1_pieces = game_copy.p1_pieces
p2_pieces = game_copy.p2_pieces
current_player = game_copy.current_player
if current_player == 1:
current_player_pieces = p1_pieces
else:
current_player_pieces = p2_pieces
turn_count += 1
fitness = 0
# add your fitness check functions here
fitness += evaluate_piece_count(current_player_pieces)
fitness += evaluate_defensive_formation(board, current_player)
fitness += evaluate_distance_to_victory(board,
current_player, current_player * -1)
fitness += evaluate_opponent_cluster_disruption(board, current_player * -1)
fitness += evaluate_potential_win_pathways(board, current_player)
fitness += get_best_move(board, current_player)[1]
return fitness
# ISHAAN BANSAL
# example function for static checks
@evaluate_runtime
def Ishaan_heatmap(game,
board,
turn_count,
attempt_number,
lastFitness):
# useful variables
current_player = game.current_player
opponent_player = -current_player # Opponent's pieces
p1_tiles = game.p1_pieces
p2_tiles = game.p2_pieces
# Initialize an 8x8 output array for the heatmap with all cells starting at zero
output = np.zeros((8, 8))
# Set cells with pieces (ally or enemy) to 0 in the heatmap
for y in range(BOARD_SIZE):
for x in range(BOARD_SIZE):
if board[y][x] != EMPTY:
output[y][x] = 0 # Cell with a piece is set to 0
# Determine clustering incentives based on fitness score (only if turn_count is not 1 or 2)
if turn_count != 1 and turn_count != 2:
ally_incentive, enemy_incentive = (
1, 3) if lastFitness < 10 else (3, 1)
else:
# Skip clustering incentive for early turns
ally_incentive, enemy_incentive = 0, 0
# Apply clustering incentives to empty cells based on adjacency to allies or enemies
for y in range(BOARD_SIZE):
for x in range(BOARD_SIZE):
if board[y][x] == EMPTY:
# Count ally and enemy neighbors in the surrounding cells (8 directions)
ally_neighbors = 0
enemy_neighbors = 0
for dy in [-1, 0, 1]:
for dx in [-1, 0, 1]:
if dy == 0 and dx == 0:
continue # Skip the cell itself
# Wrap around (toroidal)
ny, nx = (y + dy) % BOARD_SIZE, (x + dx) % BOARD_SIZE
if board[ny][nx] == current_player:
ally_neighbors += 1
elif board[ny][nx] == opponent_player:
enemy_neighbors += 1
# Apply clustering incentive based on number of adjacent ally/enemy tiles
output[y][x] += (ally_neighbors * ally_incentive) + \
(enemy_neighbors * enemy_incentive)
# Apply additional weights for lines of two adjacent pieces with a gap
directions = [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1),
(1, 1), (-1, 1), (1, -1)] # 8 directions
for y in range(BOARD_SIZE):
for x in range(BOARD_SIZE):
# Check for two pieces with a gap in all 8 directions
for dy, dx in directions:
first_y, first_x = (y + dy) % BOARD_SIZE, (x + dx) % BOARD_SIZE
gap_y, gap_x = (
first_y + dy) % BOARD_SIZE, (first_x + dx) % BOARD_SIZE
# Check if we have two pieces with a gap in the middle
if board[y][x] == current_player and board[gap_y][gap_x] == current_player and board[first_y][first_x] == EMPTY:
# Add +10 to the gap cell for two ally pieces with a gap
output[first_y][first_x] += 10
elif board[y][x] == opponent_player and board[gap_y][gap_x] == opponent_player and board[first_y][first_x] == EMPTY:
# Add +4 to the gap cell for two enemy pieces with a gap
output[first_y][first_x] += 4
return output
# ISAAC CHACKO
@evaluate_runtime
def RadialConvolution(game,
board,
turn_count,
attempt_number,
lastFitness):
# useful variables
current_player = game.current_player
p1_tiles = game.p1_pieces
p2_tiles = game.p2_pieces
output = np.zeros((8, 8))
"""
Count enemy tiles in a 5x5 window traversing an 8x8 grid with wrap-around.
Parameters:
grid (numpy.ndarray): 8x8 numpy array representing the game board
player (int): Current player (1 or -1)
Returns:
numpy.ndarray: 8x8 numpy array with counts of enemy tiles in each 5x5 window
"""
enemy = -current_player
output = np.zeros((8, 8), dtype=int)
for center_y in range(8):
for center_x in range(8):
count = 0
for i in range(-2, 3):
for j in range(-2, 3):
y = (center_y + i) % 8
x = (center_x + j) % 8
if output[y, x] == enemy:
count += 1
output[center_y, center_x] = count
return output
def combine_heatmaps(heatmap_functions,
game,
board,
turn_count,
attempt_number,
lastFitness):
"""
Combine multiple heatmaps generated by different functions.
Parameters:
heatmap_functions (list): List of functions that generate 8x8 NumPy arrays
game (Game): The current game state
Returns:
numpy.ndarray: The combined 8x8 heatmap
"""
# Initialize the result array with zeros
result = np.zeros((8, 8), dtype=float)
# Iterate through each heatmap function
for func in heatmap_functions:
# Generate the heatmap using the current function
heatmap = func(game,
board,
turn_count,
attempt_number,
lastFitness)
# Check if the generated heatmap is a valid 8x8 NumPy array
if not isinstance(heatmap, np.ndarray) or heatmap.shape != (8, 8):
raise ValueError(
f"Function {func.__name__} did not return a valid 8x8 NumPy array")
# Add the current heatmap to the result
result += heatmap
return result
def run_cases(game,
board,
turn_count,
attempt_number,
lastFitness):
if turn_count == 1 or turn_count == 2:
return False
heatmap_array = combine_heatmaps(
[Ishaan_heatmap, RadialConvolution], game, board, turn_count, attempt_number, lastFitness)
# get the shrinked list of heatmap tiles
final_heatmap = get_hot(heatmap_array)
# get the shittiest tile on the board
shitter = get_coldest(heatmap_array, game)
# generate moves for sim
possible_moves = get_possible_heatmap_moves(game, shitter, final_heatmap)
move_fitness_dict = {}
for possible_move in possible_moves:
move_fitness_dict[tuple(possible_move)] = get_fitness(game,
turn_count,
possible_move)
return move_fitness_dict
def get_best_move_and_fitness(move_fitness_dict):
"""
Get the move with the highest fitness value and its corresponding fitness from a dictionary of moves and their fitness values.
Parameters:
move_fitness_dict (dict): A dictionary where keys are moves and values are their fitness scores.
Returns:
tuple: A tuple containing (best_move, best_fitness), or (None, None) if the dictionary is empty.
"""
if not move_fitness_dict:
return None, None # Return None, None if the dictionary is empty
# Find the move with the maximum fitness value
best_move = max(move_fitness_dict, key=move_fitness_dict.get)
best_fitness = move_fitness_dict[best_move]
return best_move, best_fitness