Skip to content

Instantly share code, notes, and snippets.

@virtualanup
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save virtualanup/b8c79b2d5f72bc421d33 to your computer and use it in GitHub Desktop.
Save virtualanup/b8c79b2d5f72bc421d33 to your computer and use it in GitHub Desktop.
#!/usr/bin/python3
#Tic Tac Toe with AI (Minmax with alpha beta pruning)
#By virtualanup
#http://virtualanup.com
import pygame,random
from pygame.locals import *
from sys import exit
import math
from collections import defaultdict
#game constants
FPS=20
#color codes
black=(0,0,0)
white = (255,255,255)
blue=(0,0,200)
red=(200,0,0)
green=(0,200,0)
cross=1
circle=2
empty=0
computer=1
human=2
class Player:
'''base class for the players'''
def __init__(self,type,sign,name):
'''Initializer for player class.
Keyword Arguments:
type -- Type of the player (computer or humann)
sign -- sign assigned to the player (cross or circle)
name -- name of the player '''
self.type=type
self.sign=sign
self.name = name
def SetBoard(self,board):
'''sets the board on which the player is playing'''
self.board=board
def GetMove(self):
pass
#process click of mouse
def MouseClick(self,cell):
pass
def OppositeSign(self,sign):
'''returns the sign of the opponent'''
if sign == circle:
return cross
return circle
#the human player class. handles mouse clicks
class HumanPlayer(Player):
'''Human player class. '''
def __init__(self,sign,name):
super().__init__(human,sign,name)#human player
self.lastmove=-1
def GetMove(self):
'''Returns the next move. in case of human, it waits
until a mouse click is detected in the board.'''
if(self.lastmove != -1):
move=self.lastmove
self.lastmove=-1
return move
def MouseClick(self,cell):
'''Records the cell to which the human player checked
Keyword arguments:
cell : The cell in which mouse was clicked'''
if not cell in self.board.moves: #it no move in the position
self.lastmove=cell
class ComputerPlayer(Player):
'''Computer player class. '''
def __init__(self,sign,name):
super().__init__(computer,sign,name)#computer player
self.lastmove=-1
def GetMove(self):
'''Returns the next move. Computer player uses minmax algorithm
to determine the next move'''
#just a minor optimization : if it is the first move, always place in the center
if len(possiblemoves) == 9:
return (1,1)
self.maxdepth = 100
self.loop = 0
val,move = self.MaxValue(-9,9)
print(self.loop," moves")
return move
def GetScore(self):
'''Utility function for determining the numerical value of terminal condition.
Called in game over condition'''
if self.board.Draw():
return 0
elif self.board.GetWinner() == self.sign:
return 1
return -1 #the other player won
def MaxValue(self,alpha,beta):
'''maxvalue returns the maximized score and position for the player's move'''
maxpos = None
maxval = -9
for move in self.board.getFreePositions():
self.loop+=1
self.board.Move(move,self.sign)
if self.board.GameOver():
newval = self.GetScore()
else:
newval,movepos = self.MinValue(alpha,beta)
self.board.UndoMove()
if newval > beta:
return newval,move
if newval > alpha:
alpha = newval
if newval > maxval:
maxval = newval
maxpos = move
if newval == 1:
break;
return maxval,maxpos
def MinValue(self,alpha,beta):
'''minvalue returns the minimized score and position for the opponent's move'''
minpos = None
minval = 9
for move in self.board.getFreePositions():
self.loop+=1
self.board.Move(move,self.OppositeSign(self.sign))
if self.board.GameOver():
newval = self.GetScore()
else:
newval,movepos = self.MaxValue(alpha,beta)
self.board.UndoMove()
if newval < alpha:
return newval,move
if newval < beta:
beta = newval
if newval < minval:
minval = newval
minpos = move
if newval == -1:
break;
return minval,minpos
class BoardAnalyzer:
'''Boardanalyzer class is used as an interface to the board'''
possiblecells = [ (a,b) for a in range(0,3) for b in range(0,3)]
alllines=[[(a,b) for a in range(0,3)] for b in range(0,3)]+[[(b,a) for a in range(0,3)] for b in range(0,3)]+[[(0,0),(1,1),(2,2)],[(0,2),(1,1),(2,0)]]
def __init__(self):
self.moves=[] #this will store the moves.
self.gameover=False
self.draw=False
self.board=defaultdict(lambda:empty) #board is a dictionary that is initially empty
def getFreePositions(self):
'''Each player can place his move in any free position'''
return [x for x in self.possiblecells if not x in self.moves]
def Move(self,position,symbol):
'''move to the given position.
Keyword Arguments:
position -- position to place the move
symbol -- the symbol to place in the position'''
#if the board position is free,copy an item to it
if self.board[position] != empty:
return False
self.board[position]=symbol
self.moves.append(position)
self.CheckGameOver()
return True
def UndoMove(self):
'''Undo the previous move.'''
if len(self.moves) == 0:
return False
self.board[self.moves.pop()]=empty
#There can be no more moves after game over. So, ser gameover to false just in case
#the last move resulted in game over.
self.gameover = False
return True
def GameOver(self):
return self.gameover
def Draw(self):
return self.draw
def GetWinner(self):
'''returns the winner of the game'''
if self.GameOver() and not self.Draw():
return self.winner
def CheckGameOver(self):
'''checks the game over condition'''
for line in self.alllines:
px,py,pz=line
if self.board[px] != empty and self.board[px]==self.board[py]==self.board[pz]:
self.gameover = True
self.winner = self.board[px]
self.draw=False
break
else:
if len(self.moves) == 9:
self.draw=True
self.gameover = True
else:
self.gameover = False
class Board:
'''the actual board'''
gridcolor=blue
circlecolor=red
crosscolor=green
def __init__(self,boardsize=400):
self.players=[]
self.boardsize=boardsize
self.gameboard=BoardAnalyzer()
self.font = pygame.font.Font(None, 30) #load the font for displaying text
def reset(self):
'''resets the game board'''
self.gameboard=BoardAnalyzer()
#notify the players about the new game board
for player in self.players:
player.SetBoard(self.gameboard)
#whoever loses gets the first chance
self.player1,self.player2=self.player2,self.player1
def printstatus(self,screen):
textstr=''
if game.gameboard.GameOver():
if game.gameboard.Draw():
textstr="Draw. Click here to reset"
else:
textstr=self.player1.name+" wins. Click here to reset"
else:
textstr=self.player1.name+"'s turn"
text = self.font.render(textstr, 1,(255,255,255))
textpos = text.get_rect(centerx=screen.get_width()/2,y=self.boardsize+5)
screen.blit(text, textpos)
def AddPlayer(self,player):
player.SetBoard(self.gameboard)
self.players.append(player)
if(len(self.players) > 1):
self.player1=self.players[0]
self.player2=self.players[1]
def draw(self,screen):
#draw the board in blue color
tolerance=20
pygame.draw.line(screen, self.gridcolor, (self.boardsize/3,tolerance), (self.boardsize/3,self.boardsize-tolerance),10)
pygame.draw.line(screen, self.gridcolor, ((2*self.boardsize)/3,tolerance), ((2*self.boardsize)/3,self.boardsize-tolerance),10)
pygame.draw.line(screen, self.gridcolor, (tolerance,(self.boardsize)/3), (self.boardsize-tolerance,(self.boardsize)/3),10)
pygame.draw.line(screen, self.gridcolor, (tolerance,(2*self.boardsize)/3), (self.boardsize-tolerance,(2*self.boardsize)/3),10)
#draw each cross or circle
for move in self.gameboard.moves:
mx,my=move
onethird=int(self.boardsize/3)
if self.gameboard.board[move] == circle:
#draw a circle in that position
pos=mx*onethird+int(onethird/2),my*onethird+int(onethird/2)
pygame.draw.circle(screen, self.circlecolor, pos, int(onethird/3), 10)
elif self.gameboard.board[move] == cross:
tl=mx*onethird+int(onethird/5),my*onethird+int(onethird/5)
tr=(mx+1)*onethird-int(onethird/5),my*onethird+int(onethird/5)
bl=mx*onethird+int(onethird/5),(my+1)*onethird-int(onethird/5)
br=(mx+1)*onethird-int(onethird/5),(my+1)*onethird-int(onethird/5)
pygame.draw.line(screen, self.crosscolor, tl, br,10)
pygame.draw.line(screen, self.crosscolor, tr, bl,10)
#get items from board analyzer and draw the required items
#find the position of the mouse click and pass to the snake
def MouseClick(self,position):
#get the cell position and pass it to the player(s)
mx,my=position
if my < self.boardsize: #if inside range
onethird=int(self.boardsize/3)
cx=int(math.floor(mx/onethird))
cy=int(math.floor(my/onethird))
cell=cx,cy
self.player1.MouseClick(cell) #pass along to the player
elif self.gameboard.GameOver():
self.reset()
def update(self):
#update the board
if not self.gameboard.GameOver():
nextpos=self.player1.GetMove()
if nextpos is not None:
#move the player in that position
self.gameboard.Move(nextpos,self.player1.sign)
#if any player caused game over, keep him as player 1
if not self.gameboard.GameOver():
self.player1,self.player2=self.player2,self.player1 #exchange the players
if(__name__ == "__main__"):
pygame.init()
boardsize=400
screen = pygame.display.set_mode((boardsize,boardsize+35))
pygame.display.set_caption('Tic Tac Toe')
gameover=False
clock = pygame.time.Clock()
game=Board()
game.AddPlayer(ComputerPlayer(cross,"Anup"))
game.AddPlayer(HumanPlayer(circle,"Mohit"))
while gameover==False:
clock.tick(FPS)
screen.fill(black);
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit();exit()
if event.type == pygame.MOUSEBUTTONUP:
game.MouseClick(event.pos)
game.update()
game.draw(screen)
game.printstatus(screen)
pygame.display.update()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment