Related
I need to improve the speed of this program, because at the moment it is pretty slow. I know that representing game states in binary can be very effective, however, I don't know how to do that. I have also tried using numba, however that seems to make it slower. I have attached the code below. Thank you to anyone who can help!
import pygame, sys, time, hashlib
from copy import deepcopy
pygame.init()
red = pygame.Color(255,0,0)
white = pygame.Color(255,255,255)
black = pygame.Color(0,0,0)
pygame.display.set_caption('Hexapawn AI')
width, height = 700,700
game_window = pygame.display.set_mode((width, height))
def set_pawns():
global game_window, board
for y in range(5):
for x in range(5):
if board[y][x] == 1:
game_window.blit( blue_pawn, ( (width/5)*x, (height/5)*(4-y) ))
if board[y][x] == -1:
game_window.blit( red_pawn, ( (width/5)*x , (height/5)*(4-y) ))
def build_lines():
global game_window
for x in range(1,5):
pygame.draw.line(game_window, black, (width/5 * x, 0), (width/5 * x, height), 7)
pygame.draw.line(game_window, black, (0, height/5 * x), (width, height/5 * x), 7)
def get_possible_moves(board, player):
possible_moves = []
forward = 1 if player == 1 else -1
opponent = -1 if player == 1 else 1
for y in range(5):
for x in range(5):
if board[y][x] != player:
continue
if x-1 >= 0 and y+forward < 5 and board[y+forward][x-1] == opponent:
possible_moves.append([x,y,x-1,y+forward])
if x+1 < 5 and y+forward < 5 and board[y+forward][x+1] == opponent:
possible_moves.append([x,y,x+1,y+forward])
if (y+1 < 5 and player == 1) or (y+1 > -1 and player == -1):
if board[y+forward][x] == " ":
possible_moves.append([x,y,x,y+forward])
return possible_moves
def make_move(board,move,player):
global game_window, width, height
game_window.fill(white)
build_lines()
board[move[1]][move[0]] = " "
board[move[3]][move[2]] = player
set_pawns()
def neg_make_move(board, move, player):
x1, y1, x2, y2 = move
board = deepcopy(board)
board[y1][x1] = " "
board[y2][x2] = player
return board
def check_for_win(board,player):
if player == -1:
if -1 in board[0]:
return True
if get_possible_moves(board,1) == []:
return True
elif player == 1:
if 1 in board[4]:
return True
if get_possible_moves(board,-1) == []:
return True
return False
TRANSPOSITION_TABLE = {}
def state_hash(board):
serialized = str(board).encode()
return hashlib.sha256(serialized).hexdigest()
def store(table, board, alpha, beta, best, depth):
state = state_hash(board)
if best[1] <= alpha:
flag = 'UPPERCASE'
elif best[1] >= beta:
flag = 'LOWERCASE'
else:
flag = 'EXACT'
table[state] = [best, flag, depth]
def negamax(board, depth, turn, alpha, beta):
alpha_org = alpha
state = state_hash(board)
if state in TRANSPOSITION_TABLE:
tt_entry = TRANSPOSITION_TABLE[state]
if tt_entry[2] >= depth:
if tt_entry[1] == 'EXACT':
return tt_entry[0]
elif tt_entry[1] == 'LOWERCASE':
alpha = max(alpha, tt_entry[0][1])
elif tt_entry[1] == 'UPPERCASE':
beta = min(beta, tt_entry[0][1])
if alpha >= beta:
return tt_entry[0]
if check_for_win(board, -turn):
return None, -(25+depth)
if depth == 0:
return get_possible_moves(board,turn)[0], (depth)
best_score = -200
for move in get_possible_moves(board,turn):
new_board = neg_make_move(board, move, turn)
score = -negamax(new_board, depth - 1, -turn, -beta, -alpha)[1]
alpha = max(alpha,score)
if score > best_score:
best_score, best_move = score, move
if alpha >= beta:
break
store(TRANSPOSITION_TABLE, board, alpha_org, beta, [best_move,best_score], depth)
return best_move, best_score
# Build board
board = [[1 for x in range(5)]]
for x in range(3):
board.append([" " for x in range(5)])
board.append([-1 for x in range(5)])
game_window.fill(white)
# Draw game board lines
build_lines()
# Load sprites with correct sizes
tile_size = (width/5,height/5)
blue_pawn = pygame.transform.scale(pygame.image.load("blue_pawn.png"), tile_size)
red_pawn = pygame.transform.scale(pygame.image.load("red_pawn.png"), tile_size)
# Draw the pawns to the board
set_pawns()
pygame.display.update()
while True:
for event in pygame.event.get():
# if user clicks the X or they type esc then the screen will close
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
start = time.time()
move = negamax(board,12,1,-10000,10000)[0]
print(f"Blue move took {time.time()-start} seconds to calculate.")
make_move(board,move,1)
pygame.display.update()
if check_for_win(board,1):
print("Blue Wins!")
pygame.quit()
sys.exit()
time.sleep(1)
start = time.time()
move = negamax(board,12,-1,-10000,10000)[0]
print(f"Red move took {time.time()-start} seconds to calculate.")
make_move(board,move,-1)
pygame.display.update()
if check_for_win(board,-1):
print("Red Wins!")
pygame.quit()
sys.exit()
pygame.display.update()
time.sleep(1)
As the title suggests, I'm trying to create a Tic-Tac-Toe game using the Minimax algorithm.
My problem occurs when the computer is supposed to counter my movements. Instead of doing so, it plays in what seems to be random yet repetitive patterns.
This is my first time using the Minimax algorithm, so any help or advice would be welcomed.
Thanks in advance!
Here is my code:
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
import numpy as np
import random
class Board(GridLayout):
def __init__(self):
GridLayout.__init__(self)
self.cols = 3
self.level = 8
self.listBoard = []
array = ([0, 0, 0],
[0, 0, 0],
[0, 0, 0])
self.array = np.array(array)
for i in range(self.cols):
listLine = []
for j in range(self.cols):
cell = Button(font_size=50, on_press=self.click)
cell.i = i
cell.j = j
self.add_widget(cell)
listLine.append(cell)
self.listBoard.append(listLine)
self.check = False
start = random.randint(0, 1)
self.player = ''
self.computer = ''
# player is 1, computer is 10
if start == 1:
self.player = 'X'
self.computer = 'O'
else:
self.computer = 'X'
self.player = 'O'
move = self.minimax(self.array.copy(), self.level)
self.listBoard[move[0]][move[1]].text = self.computer
self.array[move[0]][move[1]] = 10
print(self.array)
def click(self, button):
if button.text == '' and not self.check:
button.text = self.player
self.array[button.i][button.j] = 1
print(self.array)
if self.check_victory(self.array) == 'Player':
print('Player Wins!')
self.check = True
moves = self.find_moves(self.array)
if moves.any() and not self.check:
move = self.minimax(self.array.copy(), self.level)
self.listBoard[move[0]][move[1]].text = self.computer
self.array[move[0]][move[1]] = 10
print(self.array)
elif self.check_victory(self.array) == 'None':
print('Tie!')
self.check = True
if self.check_victory(self.array) == 'Computer' and not self.check:
print('Computer Wins!')
self.check = True
moves = self.find_moves(self.array)
if not moves.any() and self.check_victory(self.array) == 'None' and not self.check:
print('Tie!')
def find_moves(self, array):
moves = np.argwhere(array == 0)
return moves
def check_victory(self, array):
temp1 = array
temp2 = np.rot90(array)
column1 = np.sum(temp1, axis=0)
column2 = np.sum(temp2, axis=0)
diagonal1 = np.diag(temp1).sum()
diagonal2 = np.diag(temp2).sum()
for i in range(3):
if column1[i] % 10 == 3 or column2[i] % 10 == 3:
return 'Player'
if column1[i] / 10 == 3 or column2[i] / 10 == 3:
return 'Computer'
if diagonal1 % 10 == 3 or diagonal2 % 10 == 3:
return 'Player'
if diagonal1 / 10 == 3 or diagonal2 / 10 == 3:
return 'Computer'
return 'None'
def minimax(self, game_state, level):
moves = self.find_moves(game_state)
best_move = moves[0]
best_score = float('-inf')
for move in moves:
clone = self.next_state(game_state.copy(), move[0], move[1], 'Computer')
score = self.min_play(clone, level - 1)
if score > best_score:
best_move = move
best_score = score
return best_move
def min_play(self, game_state, level):
if self.check_victory(game_state) == 'Player' or level == 0:
return self.evaluate(game_state)
moves = self.find_moves(game_state)
best_score = float('inf')
for move in moves:
clone = self.next_state(game_state.copy(), move[0], move[1], 'Player')
score = self.max_play(clone, level - 1)
if score < best_score:
# best_move = move
best_score = score
return best_score
def max_play(self, game_state, level):
if self.check_victory(game_state) == 'Computer' or level == 0:
return self.evaluate(game_state)
moves = self.find_moves(game_state)
best_score = float('-inf')
for move in moves:
clone = self.next_state(game_state, move[0], move[1], 'Computer')
score = self.min_play(clone, level - 1)
if score > best_score:
# best_move = move
best_score = score
return best_score
def next_state(self, array, i, j, who):
if who == 'Player':
array[i][j] = 1
else:
array[i][j] = 10
return array
def evaluate(self, array):
if self.check_victory(array) == 'Player':
return -1
if self.check_victory(array) == 'Computer':
return 1
return 0
class TestApp(App):
def build(self):
self.title = 'based graphics'
return Board()
TestApp().run()
I am trying to make a Tic-Tac-Toe game in Python using PyGame and the MiniMax algorithm. The AI plays really well when given the first chance (playing as 'X'), but becomes dumb enough to help the user win when not given the first chance (playing as 'O'). I think I know what the problem is but changing it is messing with the whole program and is not going by the given docstrings.
I've made two python files - one for the GUI (runner.py) and the other for the logic behind the game and the AI (tictactoe.py).
This is the logic behind the game:
# Import module `copy` for function `deepcopy` to deeply copy an
# original (mutable) object to save the object from mutations
import copy
X = 'X'
O = 'O'
EMPTY = None
def initial_state():
"""Returns starting state of the board
"""
return [
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]
]
def display(board, autoprint=False):
"""Displays the board nested list as
a 3x3 matrix for board visualization
"""
vis_board = ''
for row in board:
for playr in row:
if playr is None:
playr = ' '
playr += ' '
vis_board += playr
vis_board += '\n'
if autoprint:
print(vis_board)
return vis_board
def player(board):
"""Returns player who has the next turn on a board
"""
global X, O
# Initial values for every call of the function
X_count = 0
O_count = 0
for row in board:
for playr in row:
if playr == X:
X_count += 1
elif playr == O:
O_count += 1
# `X` always starts first
if O_count < X_count:
return O
return X
def actions(board):
"""Returns set of all possible actions
(i, j) available on the board
"""
global EMPTY
action_set = set()
for i, row in enumerate(board):
for j, playr in enumerate(row):
if playr is EMPTY:
action_set.add((i, j))
return action_set
def result(board, action):
"""Returns the board that results from
making move (i, j) on the board.
"""
global EMPTY
if type(action) is not tuple or len(action) != 2:
raise Exception('invalid action taken')
# Using `deepcopy` to make a deepcopy of *board*
# as duplication by slicing entire list and by
# type conversion is not working poperly
dup_board = copy.deepcopy(board)
# Unpack the coordinates as `I` and `J`
I, J = action
# Check if place has not already been used
if dup_board[I][J] is EMPTY:
dup_board[I][J] = player(dup_board)
else:
raise Exception('invalid action taken')
return dup_board
def is_full(board):
"""Returns True if all places have been occupied, else returns False
"""
global EMPTY
for row in board:
for playr in row:
if playr is EMPTY:
return False
return True
def winner(board):
"""Returns the winner of the game, if there is one.
"""
winr = None # Initial declaration to avoid errors if no winner found
# Check diagonally
if (board[1][1] == board[0][0] and board[0][0] == board[2][2])\
or (board[1][1] == board[0][2] and board[0][2] == board[2][0]):
winr = board[1][1]
return winr
for i in range(3):
# Check each row for three-in-a-row
if board[i][0] == board[i][1] and board[i][1] == board[i][2]:
winr = board[i][1]
break
# Check each column for three-in-a-column
elif board[0][i] == board[1][i] and board[1][i] == board[2][i]:
winr = board[1][i]
break
return winr
def terminal(board):
"""Returns True if game is over, False otherwise.
"""
if winner(board) is None and not is_full(board):
return False
return True
def utility(board):
"""Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
global X, O
if terminal(board):
winr = winner(board)
if winr == X:
util = 1
elif winr == O:
util = -1
else:
util = 0
return util
return None
def get_best_score(board, is_max_turn):
"""Returns the best value of values of all possible moves
"""
if utility(board) is not None:
return utility(board)
scores = []
# Recursively help `minimax` choose the best action
# in `actions` of *board* by returning the best value
for action in actions(board):
rslt = result(board, action)
scores.append(get_best_score(rslt, not is_max_turn))
return max(scores) if is_max_turn else min(scores)
def minimax(board):
"""Returns the optimal action for the current player on the board.
"""
if terminal(board):
return None
best_score = -float('inf') # Least possible score
best_action = None
for action in actions(board):
rslt = result(board, action)
score = get_best_score(rslt, False)
if score > best_score:
best_score = score
best_action = action
return best_action
The GUI code file:
# Import module `PyGame` for a GUI
import pygame
import sys
import time
# Import module `tictactoe` (from the same folder as
# this file `__file__`) for the logic of the game's AI
import tictactoe as ttt
pygame.init()
size = width, height = 600, 400
# Colors
black = (0, 0, 0)
white = (255, 255, 255)
screen = pygame.display.set_mode(size)
mediumFont = pygame.font.Font('OpenSans-Regular.ttf', 24)
largeFont = pygame.font.Font('OpenSans-Regular.ttf', 40)
moveFont = pygame.font.Font('OpenSans-Regular.ttf', 60)
user = None
board = ttt.initial_state()
ai_turn = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(black)
# Let user choose a player.
if user is None:
# Draw title
title = largeFont.render('Play Tic-Tac-Toe', True, white)
titleRect = title.get_rect()
titleRect.center = (round(width/2), 50)
screen.blit(title, titleRect)
# Draw buttons
playXButton = pygame.Rect(round(width/8), round(height/2), round(width/4), 50)
playX = mediumFont.render('Play as X', True, black)
playXRect = playX.get_rect()
playXRect.center = playXButton.center
pygame.draw.rect(screen, white, playXButton)
screen.blit(playX, playXRect)
playOButton = pygame.Rect(5*round(width/8), round(height/2), round(width/4), 50)
playO = mediumFont.render('Play as O', True, black)
playORect = playO.get_rect()
playORect.center = playOButton.center
pygame.draw.rect(screen, white, playOButton)
screen.blit(playO, playORect)
# Check if button is clicked
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
time.sleep(0.5)
if playXButton.collidepoint(mouse):
user = ttt.X
elif playOButton.collidepoint(mouse):
user = ttt.O
else:
# Draw game board
tile_size = 80
tile_origin = (width / 2 - (1.5 * tile_size),
height / 2 - (1.5 * tile_size))
tiles = []
for i in range(3):
row = []
for j in range(3):
rect = pygame.Rect(
round(tile_origin[0]+j*tile_size),
round(tile_origin[1]+i*tile_size),
round(tile_size), round(tile_size)
)
pygame.draw.rect(screen, white, rect, 3)
if board[i][j] != ttt.EMPTY:
move = moveFont.render(board[i][j], True, white)
moveRect = move.get_rect()
moveRect.center = rect.center
screen.blit(move, moveRect)
row.append(rect)
tiles.append(row)
game_over = ttt.terminal(board)
player = ttt.player(board)
# Show title
if game_over:
winner = ttt.winner(board)
if winner is None:
title = f'Game Over: Tie.'
else:
title = f'Game Over: {winner} wins.'
elif user == player:
title = f'Play as {user}'
else:
title = f'AI thinking...'
title = largeFont.render(title, True, white)
titleRect = title.get_rect()
titleRect.center = (round(width/2), 30)
screen.blit(title, titleRect)
# Check for AI move
if user != player and not game_over:
if ai_turn:
time.sleep(0.5)
move = ttt.minimax(board)
board = ttt.result(board, move)
ai_turn = False
else:
ai_turn = True
# Check for a user move
click, _, _ = pygame.mouse.get_pressed()
if click == 1 and user == player and not game_over:
mouse = pygame.mouse.get_pos()
for i in range(3):
for j in range(3):
if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)):
board = ttt.result(board, (i, j))
if game_over:
againButton = pygame.Rect(round(width/3), round(height-65), round(width/3), 50)
again = mediumFont.render('Play Again', True, black)
againRect = again.get_rect()
againRect.center = againButton.center
pygame.draw.rect(screen, white, againButton)
screen.blit(again, againRect)
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
if againButton.collidepoint(mouse):
time.sleep(0.2)
user = None
board = ttt.initial_state()
ai_turn = False
pygame.display.flip()
These are the sidenotes for the answers given by the organization that gave these questions:
No changing the no. of parameters or the parameters themselves in any functions.
Follow the docstrings written in all functions
New functions may be defined as you wish
Please let me know if there are any bugs/errors which are causing the AI to be dumb when playing as 'O'. I believe the bug is in utility, but I can't change the code because it is not allowed (written in the docstrings).
Thank you!
Edit: The problem has been ALMOST solved, but the AI becomes dumb sometimes, like not trying to block the user's move with the opposite symbol, etc.
best_score = -float('inf') # Least possible score
you need to vary this according to the player for which you calculate the move. I think because of this the negative player is choosing random/first plausible move.
I have implemented minimax and related heuristics like 2 times, and always found that using the "negamax" approach worked best, since you don't need to worry about when to apply max and when min based on the player.
I am trying to make a Tic-Tac-Toe game in Python using PyGame and the MiniMax algorithm. The AI plays really well when given the first chance (playing as 'X'), but becomes dumb enough to help the user win when not given the first chance (playing as 'O'). I think I know what the problem is but changing it is messing with the whole program and is not going by the given docstrings.
I've made two python files - one for the GUI (runner.py) and the other for the logic behind the game and the AI (tictactoe.py).
This is the logic behind the game:
# Import module `copy` for function `deepcopy` to deeply copy an
# original (mutable) object to save the object from mutations
import copy
X = 'X'
O = 'O'
EMPTY = None
def initial_state():
"""Returns starting state of the board
"""
return [
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]
]
def display(board, autoprint=False):
"""Displays the board nested list as
a 3x3 matrix for board visualization
"""
vis_board = ''
for row in board:
for playr in row:
if playr is None:
playr = ' '
playr += ' '
vis_board += playr
vis_board += '\n'
if autoprint:
print(vis_board)
return vis_board
def player(board):
"""Returns player who has the next turn on a board
"""
global X, O
# Initial values for every call of the function
X_count = 0
O_count = 0
for row in board:
for playr in row:
if playr == X:
X_count += 1
elif playr == O:
O_count += 1
# `X` always starts first
if O_count < X_count:
return O
return X
def actions(board):
"""Returns set of all possible actions
(i, j) available on the board
"""
global EMPTY
action_set = set()
for i, row in enumerate(board):
for j, playr in enumerate(row):
if playr is EMPTY:
action_set.add((i, j))
return action_set
def result(board, action):
"""Returns the board that results from
making move (i, j) on the board.
"""
global EMPTY
if type(action) is not tuple or len(action) != 2:
raise Exception('invalid action taken')
# Using `deepcopy` to make a deepcopy of *board*
# as duplication by slicing entire list and by
# type conversion is not working poperly
dup_board = copy.deepcopy(board)
# Unpack the coordinates as `I` and `J`
I, J = action
# Check if place has not already been used
if dup_board[I][J] is EMPTY:
dup_board[I][J] = player(dup_board)
else:
raise Exception('invalid action taken')
return dup_board
def is_full(board):
"""Returns True if all places have been occupied, else returns False
"""
global EMPTY
for row in board:
for playr in row:
if playr is EMPTY:
return False
return True
def winner(board):
"""Returns the winner of the game, if there is one.
"""
winr = None # Initial declaration to avoid errors if no winner found
# Check diagonally
if (board[1][1] == board[0][0] and board[0][0] == board[2][2])\
or (board[1][1] == board[0][2] and board[0][2] == board[2][0]):
winr = board[1][1]
return winr
for i in range(3):
# Check each row for three-in-a-row
if board[i][0] == board[i][1] and board[i][1] == board[i][2]:
winr = board[i][1]
break
# Check each column for three-in-a-column
elif board[0][i] == board[1][i] and board[1][i] == board[2][i]:
winr = board[1][i]
break
return winr
def terminal(board):
"""Returns True if game is over, False otherwise.
"""
if winner(board) is None and not is_full(board):
return False
return True
def utility(board):
"""Returns 1 if X has won the game, -1 if O has won, 0 otherwise.
"""
global X, O
if terminal(board):
winr = winner(board)
if winr == X:
util = 1
elif winr == O:
util = -1
else:
util = 0
return util
return None
def get_best_score(board, is_max_turn):
"""Returns the best value of values of all possible moves
"""
if utility(board) is not None:
return utility(board)
scores = []
# Recursively help `minimax` choose the best action
# in `actions` of *board* by returning the best value
for action in actions(board):
rslt = result(board, action)
scores.append(get_best_score(rslt, not is_max_turn))
return max(scores) if is_max_turn else min(scores)
def minimax(board):
"""Returns the optimal action for the current player on the board.
"""
if terminal(board):
return None
best_score = -float('inf') # Least possible score
best_action = None
for action in actions(board):
rslt = result(board, action)
score = get_best_score(rslt, False)
if score > best_score:
best_score = score
best_action = action
return best_action
The GUI code file:
# Import module `PyGame` for a GUI
import pygame
import sys
import time
# Import module `tictactoe` (from the same folder as
# this file `__file__`) for the logic of the game's AI
import tictactoe as ttt
pygame.init()
size = width, height = 600, 400
# Colors
black = (0, 0, 0)
white = (255, 255, 255)
screen = pygame.display.set_mode(size)
mediumFont = pygame.font.Font('OpenSans-Regular.ttf', 24)
largeFont = pygame.font.Font('OpenSans-Regular.ttf', 40)
moveFont = pygame.font.Font('OpenSans-Regular.ttf', 60)
user = None
board = ttt.initial_state()
ai_turn = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
screen.fill(black)
# Let user choose a player.
if user is None:
# Draw title
title = largeFont.render('Play Tic-Tac-Toe', True, white)
titleRect = title.get_rect()
titleRect.center = (round(width/2), 50)
screen.blit(title, titleRect)
# Draw buttons
playXButton = pygame.Rect(round(width/8), round(height/2), round(width/4), 50)
playX = mediumFont.render('Play as X', True, black)
playXRect = playX.get_rect()
playXRect.center = playXButton.center
pygame.draw.rect(screen, white, playXButton)
screen.blit(playX, playXRect)
playOButton = pygame.Rect(5*round(width/8), round(height/2), round(width/4), 50)
playO = mediumFont.render('Play as O', True, black)
playORect = playO.get_rect()
playORect.center = playOButton.center
pygame.draw.rect(screen, white, playOButton)
screen.blit(playO, playORect)
# Check if button is clicked
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
time.sleep(0.5)
if playXButton.collidepoint(mouse):
user = ttt.X
elif playOButton.collidepoint(mouse):
user = ttt.O
else:
# Draw game board
tile_size = 80
tile_origin = (width / 2 - (1.5 * tile_size),
height / 2 - (1.5 * tile_size))
tiles = []
for i in range(3):
row = []
for j in range(3):
rect = pygame.Rect(
round(tile_origin[0]+j*tile_size),
round(tile_origin[1]+i*tile_size),
round(tile_size), round(tile_size)
)
pygame.draw.rect(screen, white, rect, 3)
if board[i][j] != ttt.EMPTY:
move = moveFont.render(board[i][j], True, white)
moveRect = move.get_rect()
moveRect.center = rect.center
screen.blit(move, moveRect)
row.append(rect)
tiles.append(row)
game_over = ttt.terminal(board)
player = ttt.player(board)
# Show title
if game_over:
winner = ttt.winner(board)
if winner is None:
title = f'Game Over: Tie.'
else:
title = f'Game Over: {winner} wins.'
elif user == player:
title = f'Play as {user}'
else:
title = f'AI thinking...'
title = largeFont.render(title, True, white)
titleRect = title.get_rect()
titleRect.center = (round(width/2), 30)
screen.blit(title, titleRect)
# Check for AI move
if user != player and not game_over:
if ai_turn:
time.sleep(0.5)
move = ttt.minimax(board)
board = ttt.result(board, move)
ai_turn = False
else:
ai_turn = True
# Check for a user move
click, _, _ = pygame.mouse.get_pressed()
if click == 1 and user == player and not game_over:
mouse = pygame.mouse.get_pos()
for i in range(3):
for j in range(3):
if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)):
board = ttt.result(board, (i, j))
if game_over:
againButton = pygame.Rect(round(width/3), round(height-65), round(width/3), 50)
again = mediumFont.render('Play Again', True, black)
againRect = again.get_rect()
againRect.center = againButton.center
pygame.draw.rect(screen, white, againButton)
screen.blit(again, againRect)
click, _, _ = pygame.mouse.get_pressed()
if click == 1:
mouse = pygame.mouse.get_pos()
if againButton.collidepoint(mouse):
time.sleep(0.2)
user = None
board = ttt.initial_state()
ai_turn = False
pygame.display.flip()
These are the sidenotes for the answers given by the organization that gave these questions:
No changing the no. of parameters or the parameters themselves in any functions.
Follow the docstrings written in all functions
New functions may be defined as you wish
Please let me know if there are any bugs/errors which are causing the AI to be dumb when playing as 'O'. I believe the bug is in utility, but I can't change the code because it is not allowed (written in the docstrings).
Thank you!
Edit: The problem has been ALMOST solved, but the AI becomes dumb sometimes, like not trying to block the user's move with the opposite symbol, etc.
best_score = -float('inf') # Least possible score
you need to vary this according to the player for which you calculate the move. I think because of this the negative player is choosing random/first plausible move.
I have implemented minimax and related heuristics like 2 times, and always found that using the "negamax" approach worked best, since you don't need to worry about when to apply max and when min based on the player.
With a Tic Tac Toe project almost finished, I am having a few issues regarding binding and adding a tie condition. I'm able to bind functions to and and they work normally, but I want them to be bound to R and escape key (reset_game to R and close to escape). I have also tried adding a variable k for the tie condition, but it says that is has not been defined. Here's the code:
from tkinter import *
import tkinter.messagebox
ttt = Tk()
ttt.title("Tic Tac Toe")
w = Canvas(ttt, width = 902, height = 902)
w.configure (bg = "white")
w.pack()
m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
size = 300
player = 1
w.create_line(0, 300, 902, 300, fill = "black")
w.create_line(0, 601, 902, 601, fill = "black")
w.create_line(300, 0, 300, 902, fill = "black")
w.create_line(601, 0, 601, 902, fill = "black")
def on_click(event):
global m
global player
global k
row = event.y // size
col = event.x // size
if m[row][col] == 0:
cx = col * size + size // 2
cy = row * size + size // 2
if player == 1:
draw_X(cx, cy)
print ("Player 1, X")
else:
draw_O(cx, cy)
print ("Player 2, O")
m[row][col] = player
Win()
player = 2 if player == 1 else 1
k = k + 1
def draw_O(x, y):
radius = size // 3
w.create_oval(x-radius, y-radius, x+radius, y+radius, width=5, tag='cell')
def draw_X(x, y):
radius = size // 3
w.create_line(x-radius, y-radius, x+radius, y+radius, width=5, tag='cell')
w.create_line(x+radius, y-radius, x-radius, y+radius, width=5, tag='cell')
def reset_game(event):
global m
global player
w.delete('cell')
m = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
player = 1
k = 0
def tie():
if (k==8):
tkinter.messagebox.showinfo("Tic Tac Toe", "Tie")
def Win():
if (m[0][0] == m[0][1] and m[0][0] == m[0][2] and m[0][0]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][0]))
ttt.destroy()
print ("Player", str(m[0][0]), "wins")
elif (m[0][0] == m[1][0] and m[0][0] == m[2][0] and m[0][0]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][0]))
ttt.destroy()
print ("Player", str(m[0][0]), "wins")
elif (m[1][0] == m[1][1] and m[1][0] == m[1][2] and m[1][0]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[1][0]))
ttt.destroy()
print ("Player", str(m[1][0]), "wins")
elif (m[0][1] == m[1][1] and m[0][1] == m[2][1] and m[0][1]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][1]))
ttt.destroy()
print ("Player", str(m[0][1]), "wins")
elif (m[0][2] == m[1][2] and m[0][2] == m[2][2] and m[0][2]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][2]))
ttt.destroy()
print ("Player", str(m[0][2]), "wins")
elif (m[2][0] == m[2][1] and m[2][0] == m[2][2] and m[2][0]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[2][0]))
ttt.destroy()
print ("Player", str(m[2][0]), "wins")
elif (m[0][0] == m[1][1] and m[0][0] == m[2][2] and m[0][0]!=0):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][0]))
ttt.destroy()
print ("Player", str(m[0][0]), "wins")
elif (m[0][2] == m[1][1] and m[0][2] == m[2][0] and m[0][2]):
tkinter.messagebox.showinfo("Tic Tac Toe", "Winner is player " + str(m[0][2]))
ttt.destroy()
print ("Player", str(m[0][2]), "wins")
w.bind('<Button-1>', on_click)
def close(event):
ttt.destroy()
w.bind('<Button-2>',close)
w.bind('<Button-3>', reset_game)
ttt.mainloop()
I also want to stop the game after somebody wins or there's a tie without closing the canvas so I could reset. Any ideas on how to do that?
Since you have added global variable k as the number of turns played, player can be determined by value of k so it can be removed from the global variables.
You need to declare global variable k:
m = [[0,0,0], [0,0,0], [0,0,0]]
size = 300
k = 0
Also you can simplify/modify the function Win to accept the row and col as arguments and return whether there is winner:
def Win(row, col):
# check horizontal
if m[row][0] == m[row][1] == m[row][2]:
return m[row][col]
# check vertical
if m[0][col] == m[1][col] == m[2][col]:
return m[row][col]
# check diagonals
cell = (row, col)
if cell in ((0,0), (1,1), (2,2)) and m[0][0] == m[1][1] == m[2][2]:
return m[row][col]
if cell in ((2,0), (1,1), (0,2)) and m[2][0] == m[1][1] == m[0][2]:
return m[row][col]
# no winner, returns None
return None
Then modify on_click function:
def on_click(event):
global m
global k
row = event.y // size
col = event.x // size
if m[row][col] == 0:
cx = col * size + size // 2
cy = row * size + size // 2
# determine current player
player = 1 + k % 2
if player == 1:
draw_X(cx, cy)
print("Player 1, X")
else:
draw_O(cx, cy)
print("Player 2, O")
m[row][col] = player
k += 1
# only need to check winner after 5 turns
if k >= 5:
winner = Win(row, col)
msg = None
if winner:
msg = "Winner is player {}: {}".format(player, 'X' if player == 1 else 'O')
elif k == 9:
msg = "Tie game"
if msg:
tkinter.messagebox.showinfo("Tic Tac Toe", msg)
reset_game()
For the keys binding:
w.bind('<Escape>', lambda e: ttt.destroy())
w.bind('R', reset_game)
w.bind('r', reset_game)
w.focus_set() # Canvas need to get the focus in order to get the keyboard events