diff --git a/main.py b/main.py index 6692f8d..f9c4dae 100644 --- a/main.py +++ b/main.py @@ -72,6 +72,122 @@ def checkWin(board, player): if all(board[col + i][row - i] == player 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 | # =========================== @@ -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 return col -def cpu_move_provider(player, board): - col = 0 - def other_player_gonna_win(my_move: int|None = None) -> int|bool: - if my_move == None: my_move = col - my_board = deepcopy(board) - my_board[my_move][my_board[my_move].index('O')] = player +# def cpu_move_provider(player, board): +# col = 0 +# def other_player_gonna_win(my_move: int|None = None) -> int|bool: +# if my_move == None: my_move = col +# my_board = deepcopy(board) +# my_board[my_move][my_board[my_move].index('O')] = player - other_p = 'Y' if player == 'R' else 'R' - for other_p_col in range(len(my_board)): - if 'O' not in my_board[other_p_col]: - continue +# other_p = 'Y' if player == 'R' else 'R' +# for other_p_col in range(len(my_board)): +# if 'O' not in my_board[other_p_col]: +# continue - new_board = deepcopy(my_board) - 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 +# new_board = deepcopy(my_board) +# 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() +# def im_gonna_win() -> int|bool: +# for possible_col in range(len(my_board)): +# if 'O' not in my_board[other_p_col]: +# continue - time.sleep((random.random()*0.5)+0.25) # Simulate thinking time - return col +# new_board = deepcopy(my_board) +# 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 | @@ -164,7 +321,7 @@ def play_lan_client(): def play_vs_computer(): # 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 |