3
\$\begingroup\$

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 
toolic
14.7k5 gold badges29 silver badges204 bronze badges
asked Jun 30, 2017 at 22:45
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

UX

The GUI works very well.

It would be nice to explicitly show the results in the GUI, something like:

X wins!

I do see the result printed in the shell where I ran the code:

Result: X

Again, I think it would be better to print:

Result: X wins!

The GUI has a menu named "File", but that name is a bit confusing. When I see a File menu in a window, I expect to see options to open or save a file, but this is not the case. Perhaps a better name for the menu is "Actions".

Layout

In the computerMove function, this line:

e = 1; o = gamecfg.n + 1; victory = 'X'; loss = 'O'

is more commonly split into 4 lines as:

e = 1
o = gamecfg.n + 1
victory = 'X'
loss = 'O'

Naming

The PEP 8 style guide recommends snake_case for function and variable names. For example:

winCheck would be win_check

moveChoices would be move_choices

Simpler

This line:

print("Result: {}".format(result))

can be simplified using an f-string:

print(f"Result: {result}")

The same goes for other print statements as well.

answered Mar 19 at 16:58
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.