from colours import Colours as C import random import json # =========================== # | Helper functions | # =========================== def clear(): print(end='\033[2J\033[1;1H',flush=True) def colourTile(tile): try: with open("settings.json", "r") as f: settings = json.load(f) mode = settings.get("display_mode", "coloured_text") except (FileNotFoundError, json.JSONDecodeError): mode = "coloured_text" if mode == "coloured_text": if tile == 'R': return f"{C.BOLD}{C.RED} R {C.END}" elif tile == 'Y': return f"{C.BOLD}{C.YELLOW} Y {C.END}" elif tile == 'r': return f"{C.BOLD}{C.LIGHT_GREEN} R {C.END}" elif tile == 'y': return f"{C.BOLD}{C.LIGHT_GREEN} Y {C.END}" else: return " " elif mode == "coloured_background": if tile == 'R': return f"{C.BG_RED} {C.END}" elif tile == 'Y': return f"{C.BG_LIGHT_YELLOW} {C.END}" elif tile == 'r': return f"{C.BG_LIGHT_GREEN}{C.BOLD} R {C.END}" elif tile == 'y': return f"{C.BG_LIGHT_GREEN}{C.BOLD} Y {C.END}" else: return " " elif mode == "emojis": if tile == 'R': return "🔴" elif tile == 'r': return "❌" elif tile == 'Y': return "🟡" elif tile == 'y': return "⚠️" else: return " " return tile def printBoard(board): try: with open("settings.json", "r") as f: settings = json.load(f) mode = settings.get("display_mode", "coloured_text") except (FileNotFoundError, json.JSONDecodeError): mode = "coloured_text" rows = [] for i in range(6): row = "" for column in board: row += f"{C.BOLD}|{C.END}{colourTile(column[i])}" row += f"{C.BOLD}|{C.END}" rows.append(row) rows.reverse() if mode == "emojis": top = f""" {C.BOLD}CONNECT FOUR ======================{C.END}""" bottom = f"{C.BOLD}=1️⃣=2️⃣=3️⃣=4️⃣=5️⃣=6️⃣=7️⃣={C.END}" else: top = f""" {C.BOLD}CONNECT FOUR ============================={C.END}""" bottom = f"{C.BOLD}==1===2===3===4===5===6===7=={C.END}" print(f"{top}\n{'\n'.join(rows)}\n{bottom}") def getIntInput(prompt, board=None): while True: inp = input(prompt) try: inp = int(inp) if not 1 <= inp <= 7: raise ValueError return inp except ValueError: clear() if board: printBoard(board) print("Only integers 1-7 allowed") def checkWin(board, player): rows, cols = (6, 7) winCount = 4 for row in range(rows): for col in range(cols - winCount + 1): if all(board[col + i][row] == player for i in range(winCount)): return [(col + i, row) for i in range(winCount)] for col in range(cols): for row in range(rows - winCount + 1): if all(board[col][row + i] == player for i in range(winCount)): return [(col, row + i) for i in range(winCount)] for col in range(cols - winCount + 1): for row in range(rows - winCount + 1): if all(board[col + i][row + i] == player for i in range(winCount)): return [(col + i, row + i) for i in range(winCount)] for col in range(cols - winCount + 1): for row in range(winCount - 1, rows): if all(board[col + i][row - i] == player for i in range(winCount)): return [(col + i, row - i) for i in range(winCount)] def checkFull(board): if all('O' not in col for col in board): return True return False def isTerminalNode(board): if checkWin(board, 'R'): return "WinX" elif checkWin(board, 'Y'): return "WinY" elif checkFull(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 += 100000 # guaranteed win elif player_count == 3 and empty_count == 1: score += 100 elif player_count == 2 and empty_count == 2: score += 10 if opponent_count == 3 and empty_count == 1: score -= 120 # prioritize blocking opponent 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] allowedMoves.sort(key=lambda col: abs(3 - col)) # try center → edges 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 | # =========================== def local_move_provider(player, board): # return random.choice([i for i, col in enumerate(board) if 'O' in col]) col = getIntInput(f"{colourTile(player)} where do you want to drop your tile? 1-7.\n>>> ", board) - 1 return col from multiprocessing import Pool # --- New helper for multiprocessing --- def evaluate_move(args): move, board, player, depth, maximising = args newBoard = [col.copy() for col in board] tile = newBoard[move].index('O') newBoard[move][tile] = player score = minimax(newBoard, depth - 1, float('-inf'), float('inf'), not maximising) return move, score def cpu_move_provider(player, board): allowedMoves = [i for i, col in enumerate(board) if 'O' in col] # Move ordering allowedMoves.sort(key=lambda col: abs(3 - col)) try: with open("settings.json", "r") as f: settings = json.load(f) search_depth = settings.get("cpu_search_depth", 5) except (FileNotFoundError, json.JSONDecodeError): search_depth = 5 maximising = (player == 'R') # Prepare arguments for pool args_list = [(move, board, player, search_depth, maximising) for move in allowedMoves] with Pool() as pool: results = pool.map(evaluate_move, args_list) if player == 'R': return max(results, key=lambda x: x[1])[0] else: return min(results, key=lambda x: x[1])[0] # =========================== # | Main game loop | # =========================== def play_game(player1_get_move, player2_get_move): board = [['O'] * 6 for _ in range(7)] player = 'R' while True: clear() printBoard(board) if player == 'R': col = player1_get_move(player, board) else: col = player2_get_move(player, board) try: tile = board[col].index("O") except ValueError: continue board[col][tile] = player winPositions = checkWin(board, player) if winPositions: for x, y in winPositions: board[x][y] = board[x][y].lower() clear() printBoard(board) print(f"{colourTile(player)} won!") input("Press ENTER to return to the menu.") break if checkFull(board): clear() printBoard(board) print("Its a draw!") input("Press ENTER to return to the menu.") break player = 'Y' if player == 'R' else 'R' # =========================== # | Modes | # =========================== def play_local_pvp(): play_game(local_move_provider, local_move_provider) def play_lan_server(): print("PvP LAN is in maintenance due to exploits.!") input("Press ENTER to return to menu...") return def play_lan_client(): print("PvP LAN is in maintenance due to exploits.!") input("Press ENTER to return to menu...") return def play_vs_computer(): while True: inp = input("Do you want to play as red or yellow? ").lower() try: if not inp in ["r", "red", "y", "yellow"]: raise ValueError break except ValueError: print("ENTER 'r', 'red', 'y' or 'yellow'.") if inp in ["r", "red"]: play_game(local_move_provider, cpu_move_provider) elif inp in ["y", "yellow"]: play_game(cpu_move_provider, local_move_provider) # =========================== # | Menu | # =========================== def edit_settings(): settings_file = "settings.json" # Default settings if no file exists default_settings = { "display_mode": "coloured_text", # options: coloured_text, coloured_background, emojis "cpu_search_depth": 5 # options: 1-9 } # Load existing settings try: with open(settings_file, "r") as f: try: settings = json.load(f) except json.JSONDecodeError: settings = default_settings.copy() except: settings = default_settings.copy() # Keep a copy for detecting unsaved changes original_settings = settings.copy() def save_settings(): with open(settings_file, "w") as f: json.dump(settings, f, indent=4) print("Settings saved.") input("Press ENTER to return to main menu...") while True: clear() print("=== Settings Menu ===") print("1. Display Mode") print("2. CPU Search Depth") print("--------------------") print("S. Save and Exit") print("E. Exit without Saving") print() print(f"Current Settings: {settings}") choice = input("Choose a setting to edit, or Save/Exit: ").strip().lower() if choice == "1": # Display Mode submenu while True: clear() print("=== Display Mode ===") modes = [ ("coloured_text", "Coloured Text"), ("coloured_background", "Coloured Background"), ("emojis", "Emojis") ] for i, (key, label) in enumerate(modes, start=1): if settings["display_mode"] == key: print(f"{i}. {C.BOLD}{label}{C.END}") else: print(f"{i}. {label}") print("B. Go Back") sub_choice = input("Choose a display mode: ").strip().lower() if sub_choice == "b": break elif sub_choice in [str(i) for i in range(1, len(modes) + 1)]: settings["display_mode"] = modes[int(sub_choice) - 1][0] print(f"Display mode set to {settings['display_mode']}") else: input("Invalid choice. Press ENTER to try again...") elif choice == "2": # CPU Search Depth submenu while True: clear() print("=== CPU Search Depth ===") print(f"Depth: {C.BOLD}{settings["cpu_search_depth"]}{C.END}") print("B. Go Back") sub_choice = input("Choose a value 1-9, or the use + / - keys: ").strip() if sub_choice.lower() == 'b': break elif sub_choice in ["1","2","3","4","5","6","7","8","9"]: settings["cpu_search_depth"] = int(sub_choice) elif sub_choice in ['+', '-']: settings["cpu_search_depth"] = eval(f"{settings["cpu_search_depth"]} {sub_choice}1") if settings["cpu_search_depth"] > 9: settings["cpu_search_depth"] = 9 elif settings["cpu_search_depth"] < 1: settings["cpu_search_depth"] = 1 else: input("Invalid choice. Press ENTER to try again...") elif choice == "save" or choice == "s": save_settings() return elif choice == "exit" or choice == "e": if settings != original_settings: confirm = input("You have unsaved changes. Exit without saving? (y/n): ").lower() if confirm == "y": return else: return else: input("Invalid choice. Press ENTER to try again...") while True: clear() print("How do you want to play?") print("1. Player vs Player (same device)") print("2. Player vs Player (LAN)") print("3. Player vs CPU") print("4. Edit settings") print("5. Quit") choice = input("Choose 1-4: ").strip() if choice == "1": play_local_pvp() elif choice == "2": if input("Are you hosting? (y/n): ").lower() == "y": play_lan_server() else: play_lan_client() elif choice == "3": play_vs_computer() elif choice == "4": edit_settings() elif choice == "5": break else: input("Invalid choice. Press ENTER to try again...")