350 lines
10 KiB
Python
350 lines
10 KiB
Python
import os
|
|
import sys
|
|
from copy import deepcopy
|
|
from colours import Colours as C
|
|
import random
|
|
import time
|
|
|
|
# ===========================
|
|
# | Helper functions |
|
|
# ===========================
|
|
def clear():
|
|
os.system('cls' if sys.platform.startswith('win') else 'clear')
|
|
|
|
def colourTile(tile):
|
|
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 "O"
|
|
|
|
def printBoard(board):
|
|
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()
|
|
|
|
print(f""" {C.BOLD}CONNECT FOUR
|
|
============================={C.END}
|
|
{'\n'.join(rows)}
|
|
{C.BOLD}==1===2===3===4===5===6===7=={C.END}""")
|
|
|
|
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 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 |
|
|
# ===========================
|
|
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
|
|
|
|
# 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
|
|
|
|
# def im_gonna_win() -> int|bool:
|
|
# for possible_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
|
|
|
|
# # 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 |
|
|
# ===========================
|
|
def play_game(player1_get_move, player2_get_move):
|
|
board = [['O'] * 6 for _ in range(7)]
|
|
player = 'R'
|
|
|
|
while True:
|
|
clear()
|
|
printBoard(board)
|
|
|
|
# Get column from correct player
|
|
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
|
|
|
|
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():
|
|
# play_game(cpu_move_provider, local_move_provider)
|
|
play_game(local_move_provider, cpu_move_provider)
|
|
|
|
# ===========================
|
|
# | Menu |
|
|
# ===========================
|
|
while True:
|
|
clear()
|
|
print("How do you want to play?")
|
|
print("1. PvP (same device)")
|
|
print("2. PvP (LAN)")
|
|
print("3. PvC (vs computer)")
|
|
print("4. 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":
|
|
break
|
|
else:
|
|
input("Invalid choice. Press Enter to try again...")
|