that was a long session. i think minimax with alpha beta pruning works nowgit add .!

This commit is contained in:
Vincent Rodley 2025-08-12 22:13:18 +12:00
parent 3a90bbd429
commit cf288b348d

211
main.py
View File

@ -72,6 +72,122 @@ def checkWin(board, player):
if all(board[col + i][row - i] == player for i in range(winCount)): if all(board[col + i][row - i] == player for i in range(winCount)):
return [(col + i, row - i) for i in range(winCount)] return [(col + i, row - i) for i in range(winCount)]
def isTerminalNode(board):
if checkWin(board, 'R'):
return "WinX"
elif checkWin(board, 'Y'):
return "WinY"
if all('O' not in col for col in board):
return "Draw"
return False
def evalWindow(window, player):
opponent = 'Y' if player == 'R' else 'R'
player_count = window.count(player)
opponent_count = window.count(opponent)
empty_count = window.count('O')
score = 0
if player_count == 4:
score += 100
elif player_count == 3 and empty_count == 1:
score += 5
elif player_count == 2 and empty_count == 2:
score += 2
if opponent_count == 3 and empty_count == 1:
score -= 4
return score
def evalPositionForPlayer(board, player):
score = 0
# Score center column
center_col = len(board) // 2
center_array = board[center_col]
center_count = center_array.count(player)
score += center_count * 3
# Score Horizontal
for row in range(len(board[0])):
row_array = [board[col][row] for col in range(len(board))]
for col in range(len(board) - 3):
window = row_array[col:col+4]
score += evalWindow(window, player)
# Score Vertical
for col in range(len(board)):
col_array = board[col]
for row in range(len(board[col]) - 3):
window = col_array[row:row+4]
score += evalWindow(window, player)
# Score positive diagonals
for col in range(len(board) - 3):
for row in range(len(board[0]) - 3):
window = [board[col+i][row+i] for i in range(4)]
score += evalWindow(window, player)
# Score negative diagonals
for col in range(len(board) - 3):
for row in range(3, len(board[0])):
window = [board[col+i][row-i] for i in range(4)]
score += evalWindow(window, player)
return score
def evalPosition(board):
red_score = evalPositionForPlayer(board, 'R')
yellow_score = evalPositionForPlayer(board, 'Y')
return red_score - yellow_score
def minimax(board, depth, alpha, beta, maximisingPlayer):
isTerminal = isTerminalNode(board)
if isTerminal:
if isTerminal == "WinX": # Red wins
return float('inf')
elif isTerminal == "WinY": # Yellow wins
return float('-inf')
elif isTerminal == "Draw":
return 0
allowedMoves = [i for i, col in enumerate(board) if 'O' in col]
if depth == 0 or not allowedMoves:
return evalPosition(board)
if maximisingPlayer:
maxEval = float('-inf')
for move in allowedMoves:
newPosition = [col.copy() for col in board]
tile = newPosition[move].index("O")
newPosition[move][tile] = 'R'
evaluation = minimax(newPosition, depth - 1, alpha, beta, False)
maxEval = max(maxEval, evaluation)
alpha = max(alpha, evaluation)
if beta <= alpha:
break
return maxEval
else:
minEval = float('inf')
for move in allowedMoves:
newPosition = [col.copy() for col in board]
tile = newPosition[move].index("O")
newPosition[move][tile] = 'Y'
evaluation = minimax(newPosition, depth - 1, alpha, beta, True)
minEval = min(minEval, evaluation)
beta = min(beta, evaluation)
if beta <= alpha:
break
return minEval
# =========================== # ===========================
# | Player move providers | # | Player move providers |
# =========================== # ===========================
@ -79,36 +195,77 @@ def local_move_provider(player, board):
col = getIntInput(f"{colourTile(player)} where do you want to drop your tile? 1-7.\n>>> ", board) - 1 col = getIntInput(f"{colourTile(player)} where do you want to drop your tile? 1-7.\n>>> ", board) - 1
return col return col
def cpu_move_provider(player, board): # def cpu_move_provider(player, board):
col = 0 # col = 0
def other_player_gonna_win(my_move: int|None = None) -> int|bool: # def other_player_gonna_win(my_move: int|None = None) -> int|bool:
if my_move == None: my_move = col # if my_move == None: my_move = col
my_board = deepcopy(board) # my_board = deepcopy(board)
my_board[my_move][my_board[my_move].index('O')] = player # my_board[my_move][my_board[my_move].index('O')] = player
other_p = 'Y' if player == 'R' else 'R' # other_p = 'Y' if player == 'R' else 'R'
for other_p_col in range(len(my_board)): # for other_p_col in range(len(my_board)):
if 'O' not in my_board[other_p_col]: # if 'O' not in my_board[other_p_col]:
continue # continue
new_board = deepcopy(my_board) # new_board = deepcopy(my_board)
new_board[other_p_col][new_board[other_p_col].index('O')] = other_p # new_board[other_p_col][new_board[other_p_col].index('O')] = other_p
if checkWin(new_board, other_p) != None: # if checkWin(new_board, other_p) != None:
return other_p_col # return other_p_col
return False # return False
# Start with a random move # def im_gonna_win() -> int|bool:
col = random.randint(0,6) # for possible_col in range(len(my_board)):
while not any([t == 'O' for t in board[col]]): # if 'O' not in my_board[other_p_col]:
col += 1 # continue
if col == 7: col = 0
# Prevent other player winning 1 move deep
if other_player_gonna_win():
col = other_player_gonna_win()
time.sleep((random.random()*0.5)+0.25) # Simulate thinking time # new_board = deepcopy(my_board)
return col # new_board[other_p_col][new_board[other_p_col].index('O')] = other_p
# if checkWin(new_board, other_p) != None:
# return other_p_col
# return False
# # Start with a random move
# col = random.randint(0,6)
# while not any([t == 'O' for t in board[col]]):
# col += 1
# if col == 7: col = 0
# # Prevent other player winning 1 move deep
# if other_player_gonna_win():
# col = other_player_gonna_win()
# time.sleep((random.random()*0.5)+0.25) # Simulate thinking time
# return col
def cpu_move_provider(player, board):
allowedMoves = [i for i, col in enumerate(board) if 'O' in col]
best_score = float('-inf') if player == 'R' else float('inf')
best_move = None
search_depth = 5
maximising = True if player == 'R' else False
for move in allowedMoves:
newBoard = [col.copy() for col in board]
tile = newBoard[move].index('O')
newBoard[move][tile] = player
score = minimax(newBoard, search_depth - 1, float('-inf'), float('inf'), not maximising) # because next move is opponent's turn
if player == 'R':
if score > best_score:
best_score = score
best_move = move
else:
if score < best_score:
best_score = score
best_move = move
if best_move is None:
best_move = random.choice(allowedMoves)
return best_move
# =========================== # ===========================
# | Main game loop | # | Main game loop |
@ -164,7 +321,7 @@ def play_lan_client():
def play_vs_computer(): def play_vs_computer():
# play_game(cpu_move_provider, local_move_provider) # play_game(cpu_move_provider, local_move_provider)
play_game(cpu_move_provider, cpu_move_provider) play_game(local_move_provider, cpu_move_provider)
# =========================== # ===========================
# | Menu | # | Menu |