├── AIClass.py ├── Connect4Client.py ├── GameBoard.py ├── PlayerClasses.py └── README.md /AIClass.py: -------------------------------------------------------------------------------- 1 | import math 2 | from PlayerClasses import Player 3 | 4 | INFINITY = math.inf 5 | class AI(Player): 6 | depth = 0 7 | currentDepth = 0 8 | showScores = False 9 | def __init__(self, chip='X', difficulty=1, showScores='n'): 10 | super(AI, self).__init__(chip) 11 | self.setDifficulty(difficulty) 12 | self.logScores(showScores) 13 | 14 | def setDifficulty(self, difficulty): 15 | self.depth = difficulty 16 | 17 | def logScores(self, showScores): 18 | if showScores == 'y': 19 | self.showScores = True 20 | 21 | def playTurn(self, board): 22 | move = self.alphaBetaSearch(board) 23 | board.addChip(self.chip, move[0], move[1]) 24 | return move 25 | 26 | # returns tuple of (row, column) 27 | def generateMoves(self, board): 28 | possibleMoves = [] #list of possible positions 29 | for column in range(board.boardWidth): 30 | move = board.canAddChip(column) 31 | if move[0]: #if chip can be added 32 | possibleMoves.append((move[1], column)) # (row, column) 33 | return possibleMoves 34 | 35 | 36 | def evaluateHeuristic(self, board): 37 | 38 | horizontalScore = 0 39 | verticalScore = 0 40 | diagonal1Score = 0 41 | diagonal2Score = 0 42 | 43 | ''' // Vertical 44 | // Check each column for vertical score 45 | // 46 | // 3 pssible situations per column 47 | // 0 1 2 3 4 5 6 48 | // [x][ ][ ][ ][ ][ ][ ] 0 49 | // [x][x][ ][ ][ ][ ][ ] 1 50 | // [x][x][x][ ][ ][ ][ ] 2 51 | // [x][x][x][ ][ ][ ][ ] 3 52 | // [ ][x][x][ ][ ][ ][ ] 4 53 | // [ ][ ][x][ ][ ][ ][ ] 5 54 | ''' 55 | 56 | for row in range(board.boardHeight - 3): 57 | for column in range(board.boardWidth): 58 | score = self.scorePosition(board, row, column, 1, 0) 59 | verticalScore += score 60 | 61 | ''' 62 | // Horizontal 63 | // Check each row's score 64 | // 65 | // 4 possible situations per row 66 | // 0 1 2 3 4 5 6 67 | // [x][x][x][x][ ][ ][ ] 0 68 | // [ ][x][x][x][x][ ][ ] 1 69 | // [ ][ ][x][x][x][x][ ] 2 70 | // [ ][ ][ ][x][x][x][x] 3 71 | // [ ][ ][ ][ ][ ][ ][ ] 4 72 | // [ ][ ][ ][ ][ ][ ][ ] 5 73 | ''' 74 | for row in range(board.boardHeight): 75 | for column in range(board.boardWidth - 3): 76 | score = self.scorePosition(board, row, column, 0, 1) 77 | horizontalScore += score 78 | 79 | ''' // Diagonal points 1 (negative-slope) 80 | // 81 | // 82 | // 0 1 2 3 4 5 6 83 | // [x][ ][ ][ ][ ][ ][ ] 0 84 | // [ ][x][ ][ ][ ][ ][ ] 1 85 | // [ ][ ][x][ ][ ][ ][ ] 2 86 | // [ ][ ][ ][x][ ][ ][ ] 3 87 | // [ ][ ][ ][ ][ ][ ][ ] 4 88 | // [ ][ ][ ][ ][ ][ ][ ] 5 89 | ''' 90 | for row in range(board.boardHeight-3): 91 | for column in range(board.boardWidth - 3): 92 | score = self.scorePosition(board, row, column, 1, 1) 93 | diagonal1Score += score 94 | 95 | ''' 96 | // Diagonal points 2 (positive slope) 97 | // 98 | // 99 | // 0 1 2 3 4 5 6 100 | // [ ][ ][ ][x][ ][ ][ ] 0 101 | // [ ][ ][x][ ][ ][ ][ ] 1 102 | // [ ][x][ ][ ][ ][ ][ ] 2 103 | // [x][ ][ ][ ][ ][ ][ ] 3 104 | // [ ][ ][ ][ ][ ][ ][ ] 4 105 | // [ ][ ][ ][ ][ ][ ][ ] 5 106 | ''' 107 | for row in range(3, board.boardHeight): 108 | for column in range(board.boardWidth - 3): 109 | score = self.scorePosition(board, row, column, -1, 1) 110 | diagonal2Score += score 111 | 112 | return horizontalScore + verticalScore + diagonal1Score + diagonal2Score 113 | 114 | def scorePosition(self, board, row, column, deltaROW, deltaCOL): 115 | ''' 116 | Hueristic evaluation for current state 117 | +1000, +100, +10, +1 for 4-,3-,2-,1-in-a-line for AI player 118 | -1000, -100, -10, -1 for 4-,3-,2-,1-in-a-line for human player 119 | 0 otherwise 120 | ''' 121 | humanScore = 0 122 | AIScore = 0 123 | humanPoints = 0 124 | AIPoints = 0 125 | for i in range(4): 126 | currentChip = board.getChip(row, column) 127 | if currentChip == self.chip: #if current chip is AI 128 | AIPoints += 1 129 | elif currentChip == 'O': #player chip 130 | humanPoints += 1 131 | # empty otherwise 132 | row += deltaROW 133 | column += deltaCOL 134 | 135 | if humanPoints == 1: 136 | humanScore = -1 # -1 point 137 | elif humanPoints == 2: 138 | humanScore = -10 # -10 points 139 | elif humanPoints == 3: 140 | humanScore = -100 # -100 points 141 | elif humanPoints == 4: 142 | humanScore = -1000 # -1000 points 143 | # 0 otherwise 144 | 145 | if AIPoints == 1: 146 | AIScore = 1 # 1 point 147 | elif AIPoints == 2: 148 | AIScore = 10 # 10 points 149 | elif AIPoints == 3: 150 | AIScore = 100 # 100 points 151 | elif AIPoints == 4: 152 | AIScore = 1000 # 1000 points 153 | # 0 otherwise 154 | 155 | return humanScore + AIScore 156 | 157 | 158 | 159 | def alphaBetaSearch(self, state): 160 | self.currentDepth = 0 161 | scores = [] 162 | bestAction = None 163 | v = max_value = -INFINITY 164 | alpha = -INFINITY 165 | beta = INFINITY 166 | actions = self.generateMoves(state) 167 | for action in actions: 168 | state.addChip(self.chip, action[0], action[1]) 169 | v = self.minValue(state, alpha, beta) 170 | scores.append(v) 171 | if self.showScores: print("SCORE: ", v) 172 | if v > max_value: 173 | bestAction = action 174 | max_value = v 175 | alpha = max(alpha, max_value) 176 | self.currentDepth -= 1 177 | state.removeChip(action[0], action[1]) 178 | if len(scores) == 1: 179 | bestAction = actions[0] 180 | return bestAction 181 | 182 | def maxValue(self, state, alpha, beta): 183 | self.currentDepth += 1 184 | actions = self.generateMoves(state) 185 | if not actions or self.currentDepth >= self.depth: #if list of next moves is empty or or reached root 186 | score = self.evaluateHeuristic(state) 187 | return score 188 | else: 189 | v = -INFINITY 190 | for action in actions: 191 | state.addChip(self.chip, action[0], action[1]) 192 | v = max(v, self.minValue(state, alpha, beta) ) 193 | if v >= beta: 194 | self.currentDepth -= 1 195 | state.removeChip(action[0], action[1]) 196 | return v 197 | alpha = max(v, alpha) 198 | self.currentDepth -= 1 199 | state.removeChip(action[0], action[1]) 200 | return v 201 | 202 | def minValue(self, state, alpha, beta): 203 | self.currentDepth += 1 204 | actions = self.generateMoves(state) 205 | if not actions or self.currentDepth >= self.depth: #if list of next moves is empty or or reached root 206 | score = self.evaluateHeuristic(state) 207 | return score 208 | else: 209 | v = INFINITY 210 | for action in actions: 211 | state.addChip('O', action[0], action[1]) 212 | v = min(v, self.maxValue(state, alpha, beta) ) 213 | if v <= alpha: 214 | self.currentDepth -= 1 215 | state.removeChip(action[0], action[1]) 216 | return v 217 | beta = min(v, beta) 218 | self.currentDepth -= 1 219 | state.removeChip(action[0], action[1]) 220 | return v -------------------------------------------------------------------------------- /Connect4Client.py: -------------------------------------------------------------------------------- 1 | from PlayerClasses import Player, Human 2 | from AIClass import AI 3 | from GameBoard import GameBoard 4 | 5 | class GameClient: 6 | board = None 7 | human = None 8 | ai = None 9 | winnerFound = False 10 | humansTurn = True 11 | currentRound = 1 12 | MAX_ROUNDS = 42 #max number of turns before game baord is full 13 | 14 | def __init__(self): 15 | self.board = GameBoard() 16 | self.human = Human('O') 17 | difficulty = int(input("Enter a difficulty from 1 to 6.\nYou can go higher, but performance will take longer.\n> ")) 18 | showScores = input("Show scores? (y/n)> ") 19 | self.ai = AI('X', difficulty, showScores) 20 | 21 | def play(self): 22 | print("Playing game...") 23 | self.board.printBoard() 24 | winner = "It's a DRAW!" 25 | while self.currentRound <= self.MAX_ROUNDS and not self.winnerFound: 26 | if self.humansTurn: 27 | print("Player's turn...") 28 | playedChip = self.human.playTurn(self.board) 29 | self.winnerFound = self.board.isWinner(self.human.chip) 30 | if self.winnerFound: winner = "PLAYER wins!" 31 | self.humansTurn = False 32 | print("Player played chip at column ", playedChip[1]+1) 33 | else: 34 | print("AI's turn...") 35 | playedChip = self.ai.playTurn(self.board) 36 | self.winnerFound = self.board.isWinner(self.ai.chip) 37 | if self.winnerFound: winner = "AI wins!" 38 | self.humansTurn = True 39 | print("AI played chip at column ", playedChip[1]+1) 40 | self.currentRound += 1 41 | self.board.printBoard() 42 | return winner 43 | 44 | def reset(self): 45 | #reset variables 46 | self.currentRound = 1 47 | self.winnerFound = False 48 | self.humansTurn = True 49 | self.board.resetBoard() 50 | difficulty = int(input("Enter a difficulty from 1 to 6.\nYou can go higher, but performance will take longer.\n> ")) 51 | self.ai.setDifficulty(difficulty) 52 | 53 | def endGame(winner): 54 | print(winner, end=" ") 55 | userInput = input("Play again? (y/n)\n") 56 | return True if userInput == 'y' else False 57 | 58 | if __name__ == "__main__": 59 | gameClient = GameClient() 60 | winner = gameClient.play() 61 | playAgain = endGame(winner) 62 | while playAgain: 63 | gameClient.reset() 64 | winner = gameClient.play() 65 | playAgain = endGame(winner) 66 | -------------------------------------------------------------------------------- /GameBoard.py: -------------------------------------------------------------------------------- 1 | class GameBoard: 2 | board = [] 3 | boardWidth = 7 4 | boardHeight = 6 5 | 6 | def __init__(self): 7 | self.setBoard() 8 | 9 | def setBoard(self): 10 | for row in range(self.boardHeight): 11 | self.board.append([]) 12 | for column in range(self.boardWidth): 13 | self.board[row].append('-') 14 | 15 | def resetBoard(self): 16 | for row in range(self.boardHeight): 17 | for column in range(self.boardWidth): 18 | self.board[row][column] = '-' 19 | 20 | def printBoard(self): 21 | for i in range(self.boardHeight): 22 | print("| ", end="") 23 | print(*self.board[i], sep=" | ", end="") 24 | print(" |\n") 25 | 26 | def isValidColumn(self, column): 27 | return False if column < 0 or column >= self.boardWidth else True 28 | 29 | def getChip(self, row, column): 30 | return self.board[row][column] 31 | 32 | '''Check if there is room to add in a chip 33 | return row if chip can be added''' 34 | def canAddChip(self, column): 35 | for i in range( (self.boardHeight-1), -1, -1): #from 5 to 0 36 | if self.board[i][column] == '-': 37 | return True, i 38 | return False, -1 39 | 40 | def addChip(self, chip, row, column): 41 | '''check if there is room for a chip to add 42 | starting from bottom, return true if spot empty, false otherwise 43 | ''' 44 | self.board[row][column] = chip 45 | 46 | def removeChip(self, row, column): 47 | self.board[row][column] = '-' 48 | 49 | def isWinner(self, chip): 50 | 51 | ticks = 0 52 | # vertical 53 | for row in range(self.boardHeight - 3): 54 | for column in range(self.boardWidth): 55 | ticks = self.checkAdjacent(chip, row, column, 1, 0) 56 | if ticks == 4: return True 57 | # horizontal 58 | for row in range(self.boardHeight): 59 | for column in range(self.boardWidth - 3): 60 | ticks = self.checkAdjacent(chip, row, column, 0, 1) 61 | if ticks == 4: return True 62 | # positive slope diagonal 63 | for row in range(self.boardHeight-3): 64 | for column in range(self.boardWidth - 3): 65 | ticks = self.checkAdjacent(chip, row, column, 1, 1) 66 | if ticks == 4: return True 67 | #negative slope diagonal 68 | for row in range(3, self.boardHeight): 69 | for column in range(self.boardWidth - 5): 70 | ticks = self.checkAdjacent(chip, row, column, -1, 1) 71 | if ticks == 4: return True 72 | return False 73 | 74 | def checkAdjacent(self, chip, row, column, deltaROW, deltaCOL): 75 | count = 0 76 | for i in range(4): 77 | currentChip = self.getChip(row, column) 78 | if currentChip == chip: 79 | count += 1 80 | row += deltaROW 81 | column += deltaCOL 82 | return count -------------------------------------------------------------------------------- /PlayerClasses.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from abc import ABC, ABCMeta, abstractmethod 3 | 4 | #abstract class for human and AI players 5 | class Player(metaclass=ABCMeta): 6 | chip = "" #'O' or 'X' 7 | 8 | def __init__(self, chip): 9 | self.chip = chip 10 | 11 | @abstractmethod 12 | def playTurn(self): 13 | pass 14 | 15 | class Human(Player): 16 | def __init__(self, chip): 17 | super(Human, self).__init__(chip) 18 | 19 | def playTurn(self, board): 20 | column = int(input("Pick a column (enter -1 to quit playing) > ")) 21 | if column == -1: sys.exit() 22 | column -= 1 23 | while True: 24 | if board.isValidColumn(column): 25 | row = board.canAddChip(column) #tuple (can add chip[bool], row) 26 | if row[0]: 27 | #attempt to add chip, ask input if failed 28 | board.addChip(self.chip, row[1], column) 29 | break 30 | column = int(input("That column did not work. Try a different column > ")) 31 | column -= 1 32 | return row[1], column 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Connect Four 2 | ============== 3 | 4 | Forrest Knight 5 | 6 | CS 480 - Artificial Intelligence - Fall 2017 7 | 8 | *Python 3.6.0* 9 | 10 | Usage 11 | ------------ 12 | 13 | Run Command: 14 | 15 | python Connect4Client.py 16 | 17 | Enter a number representing the difficulty of the AI. 18 | - Larger Number = Better AI Score/Worse Human Score 19 | - Smaller Number = Worse AI Score/Better Human Score 20 | 21 | --------------------------------------------------------------------------------