So, I've tried to write a gui for Tic Tac Toe or noughts and crosses, as well as an AI opponent to play against. This is by far the longest program I've written, having mainly just done bash scripts as part of my job, and occasionally using a few lines of python to do something that is unpleasant to do in Bash, but I do have to read and understand a fair bit of Python code at work.
I would appreciate any feedback, and do plan to implement some sort of recursive search into the engine, rather than the current fixed depth, as my real goal is to write a gui and engine for Tak, a much more complicated game.
import tkinter as tk
import random
import numpy as np
import gamecfg
# The main class
class Square(tk.Canvas):
"""A Square is a canvas on which nought or cross can be played"""
# Cross starts, board of length m, and the game has no result yet
crossToPlay = True
result = None
m = gamecfg.n
# moveList, squareDict and state are all representations of the board
moveList = []
squareDict = {}
state = np.zeros((m, m))
def __init__(self, name, master=None, size=None):
super().__init__(master, width=size, height=size)
self.bind("<Button-1>", self.tic)
self.config(highlightbackground="Black")
self.config(highlightthickness=1)
self.symbol = None
self.name = name
self.topLeft = size * 0.15
self.bottomRight = size * 0.85
# Add itself to the dict of squares
Square.squareDict[self.name] = self
def draw(self):
"""This will draw a nought or cross on itself,
depending on who is to play."""
if not self.symbol and not self.result:
tl = self.topLeft
br = self.bottomRight
if Square.crossToPlay:
self.create_line(tl, tl, br, br)
self.create_line(tl, br, br, tl)
self.symbol = 'X'
Square.state[self.name] = 1
else:
self.create_oval(tl, tl, br, br)
self.symbol = 'O'
Square.state[self.name] = Square.m + 1
Square.crossToPlay = not Square.crossToPlay
Square.moveList.append(self)
Square.print()
Square.setResult(winCheck(Square.state))
def tic(self, event):
""""A tic is a player clicking on a square"""
self.draw()
computerMove()
def tac(self):
"""A tac is the computer playing"""
self.master.update()
self.after(gamecfg.engineWait, self.draw())
def clear(self):
""""This will clear the selected Square."""
if self.symbol:
self.delete("all")
self.symbol = None
Square.result = None
Square.crossToPlay = not Square.crossToPlay
Square.print()
Square.state[self.name] = 0
@classmethod
def undo(cls):
cls.moveList.pop().clear()
@classmethod
def print(cls):
print('Moves:', *[square.name for square in cls.moveList])
@classmethod
def setResult(cls, result):
if result:
cls.result = result
print("Result: {}".format(result))
def winCheck(state):
"""Takes a position, and returns the outcome of that game"""
# Sums which correspond to a line across a column
winNums = list(state.sum(axis=0))
# Sums which correspond to a line across a row
winNums.extend(list(state.sum(axis=1)))
# Sums which correspond to a line across the main diagonal
winNums.append(state.trace())
# Sums which correspond to a line across the off diagonal
winNums.append(np.flipud(state).trace())
if Square.m in winNums:
return 'X'
elif (Square.m**2 + Square.m) in winNums:
return 'O'
elif np.count_nonzero(state) == Square.m**2:
return 'D'
else:
return None
# This function is called safely anytime it might be the computer's turn
def computerMove():
# Decide whether or not the computer is to play
if gamecfg.engineIsCross == Square.crossToPlay and not Square.result:
# Set the value of engine and opponent's pieces in state
if gamecfg.engineIsCross:
e = 1; o = gamecfg.n + 1; victory = 'X'; loss = 'O'
else:
e = gamecfg.n + 1; o = 1; victory = 'O'; loss = 'X'
# Use Square.state to determine the legal moves
gameState = Square.state.copy()
moveChoices = []
moveScores = {}
# Iterate over state, to determine which squares are empty
it = np.nditer(gameState, flags=['multi_index'])
while not it.finished:
if it[0] == 0:
moveChoices.append(it.multi_index)
it.iternext()
for move in moveChoices:
# Before evaluation, all squares are equal
moveScores[move] = 0
if gamecfg.engineLevel >= 2:
for move in moveChoices:
moveState = gameState.copy()
moveState[move] = e
#print("I am considering {}".format(move))
if winCheck(moveState) == victory:
# Winning is always the best possible move
print("I will play {} to win!".format(move))
moveScores[move] = 100
break
if gamecfg.engineLevel >= 3:
movesLeft = moveChoices[:]
movesLeft.remove(move)
for next in movesLeft:
nextState = moveState.copy()
nextState[next] = o
if winCheck(nextState) == loss:
# The only thing preferable to blocking a loss is winning
print("I will play {} to not lose!".format(next))
moveScores[next] = 10
break
# Choose the highest value, or a random move if all are tied
if all(moveScore == 0 for moveScore in moveScores.values()):
Square.squareDict[random.choice(moveChoices)].tac()
else:
Square.squareDict[max(moveScores.keys(), key=(lambda k: moveScores[k]))].tac()
def clearAll():
while Square.moveList:
Square.moveList.pop().clear()
computerMove()
def main():
root = tk.Tk()
root.title("Tic Tac Toe")
# Creating the board, 600 x 600 pixels, n squares across
m = gamecfg.n
size = 600 // m
squares = [(rank, file) for rank in range(m) for file in range(m)]
for (rank, file) in squares:
square = Square((rank, file), master=root, size=size)
square.grid(row=rank, column=file)
# Creating File Menu
menu = tk.Menu(root)
root.config(menu=menu)
fileMenu = tk.Menu(menu)
menu.add_cascade(label="File", menu=fileMenu)
# Undo calls the clear function on the most recently played square.
fileMenu.add_command(label="Undo", command=lambda: Square.moveList.pop().clear())
fileMenu.add_command(label="State", command=lambda: print(Square.state))
fileMenu.add_command(label="Result", command=lambda: print(Square.result))
fileMenu.add_command(label="Restart", command=lambda: clearAll())
computerMove()
root.mainloop()
if __name__ == '__main__':
main()
And gamecfg.py is not really any code, just some config settings:
# Length of board
n = 3
#Engine player: True - X, False - O, None - No Engine
engineIsCross = True
# Level 1: Random legal move, Level 2: Wins if possible, Level 3: Avoids loss if possible as well
engineLevel = 3
# How long in miliseconds the engine should sleep between calculating it's move and drawing it
engineWait = 500