Last active
August 29, 2015 14:01
-
-
Save virtualanup/555fad9e5c4c5145d773 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
#Tic Tac Toe with AI (Minmax) | |
#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''' | |
self.loop = 0 | |
val,move = self.MaxValue() | |
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): | |
'''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() | |
self.board.UndoMove() | |
if newval > maxval: | |
maxval = newval | |
maxpos = move | |
return maxval,maxpos | |
def MinValue(self): | |
'''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() | |
self.board.UndoMove() | |
if newval < minval: | |
minval = newval | |
minpos = move | |
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