Advertisement

tic-tac-toe review request

Started by July 29, 2017 06:59 PM
4 comments, last by Alberth 7 years, 4 months ago

I was looking for an opportunity to learn/improve skills so I wrote this simple Tic-Tac-Toe game.

I have written Python code for simple network tests and know enough JavaScript to get by. Still not certain on fine details though, particularly what subtle (or not) best practices I am likely missing.

Code review, testing and advice on how to improve would be appreciated.

Code is running on PythonAnywhere here:
http://coffeeaddict.pythonanywhere.com/tictactoe

Code is here on GIT:
https://github.com/coffeeaddict19/tictactoe

The three pieces are:
JavaScript and Markup: tictactoe_page.html
Bottle Framework Implementation: tictactoe_bottle.py
Python Game Logic: tictactoe_logic.py

If you look closely at the game logic the AI is...primitive. Researching MinMax to improve it is the next thing on list but in my experience if I wait for perfection I never get anything done.

Anyway...how can I improve this game and this code?

Thanks,
John

You say the AI is primitive but it is impossible to beat. Is that intentional? Maybe you can add a small chance for the AI to make a mistake, as an 'improvement'.

Advertisement

# Lines with a line number in front refer to your code, everything else is my comment.

     5	        self.gamestate = 0 #0=in play;1=win;2=tie
     8	        self.current_player_turn = 0 #0=player1;1=player2

# Define some names for the game state.

IN_PLAY = 0
WIN = 1
TIE = 2

PLAYER1 = "player 1"
PLAYER2 = "player 2"

self.gamestate = IN_PLAY
self.current_player_turn = PLAYER1  # Nothing wrong with using strings as value of a constant.


     9	        print 'init'

# In Python 3 this will fail, add parentheses to prepare yourself for the
# transition.

print('init')


    11	    def validatemove(self, move):

# Add a doc-string describing the function interfaces, eg

    def validatemove(self, move):
        """
        Verify that the move is valid.

        @param move: Move to verify.
        @type  move: C{int}

        @return: Whether the move was valid.
        @rtype:  C{bool}
        """

# There are several different styles of documenting, I used EpyDoc. Note that
# Python 3 will support typed parameters and return values, making the @type and
# @rtype entries mostly obsolete.

# I know this may seem totally useless to you, and for this function I arguably
# agree with you. The point is however, in a year, you don't know what each
# function is doing, nor do you remember the types of the arguments or the
# special edge-cases that apply in calling a function.
# That means you will have to reverse-engineer that information from the code,
# when you open this file again. Reverse engineering information is always
# slower and more error-prone than just reading exactly what you want to know.
#
# Documentation as such is thus an investment for the future that makes you work
# faster and more accurately tomorrow.

    12	        print 'validating move', move

# Python 3 will want something like

print('validating move {}'.format(move))


    13	        #is the 'move' in range?
    14	        if not self.gamestate == 0:
    15	            print 'game is not in play'
    16	            return False
 
    17	        if move > 8 or move < 0:
    18	            print 'move is not in range'
    19	            return False
 
    20	        if move in self.player1_moves | self.player2_moves:
    21	            print 'play already made'
    22	            return False

    23	        print 'move', move, 'evaluated as legal'
    24	        return True

# If you insert empty lines between the different checks, it's easier to see
# the steps you're making.
#
# The problem you'll have here is that an empty line between functions and an
# empty line between statements looks very similar. This is also where function
# documentation helps. It adds a big different looking piece of text with each
# function, making it easier to see where a function starts in the code.

    26	    def getgamestate(self):
    27	        if self.gamestate == 0:
    28	            return "in play"
    29	        elif self.gamestate == 1:
    30	            return "win"
    31	        else:
    32	            return "tie"

# I tend to be more paranoid, and pretty much always check for completeness, ie

if self.gamestate == 0:
    return "in play"
elif self.gamestate == 1:
    return "win"
else:
    assert self.gamestate == 2
    return "tie"

# It's surprising how often these checks trigger, because you forgot some case somewhere.
# 
# A simpler way to do the above is
 
GAMESTATE_DESCRIPTIONS = {
    0: 'in play',
    1: 'win',
    2: 'tie',
}

def getgamestate(self):
    """
    Get a human-readable description of the game state.

    @return: Description of the current state of the game.
    @rtype:  C{str}
    """
    return GAMESTATE_DESCRIPTIONS[self.gamestate]

# I just document all functions to avoid the problem of deciding what is
# trivial enough not to document, ymmv.


    34	    def getcurrentplayerturn(self):
    35	        if self.current_player_turn == 0:
    36	            return "player 1"
    37	        elif self.current_player_turn == 1:
    38	            return "player 2"

# With the string constants, this would be just

def getcurrentplayerturn(self):
    """
    Get the name of the current player.

    @return: Name of the current player.
    @rtype:  C{str}
    """
    return self.current_player_turn

# And yes, it's normal that the documentation is longer than the code in
# Python, the language is very efficient in number of lines needed to express
# something.

    54	    
    55	    def evaluateWinState(self,movesetToEval):
    56	        #012
    57	        #345
    58	        #678
    59	        winStateList = [{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}]

winStateList = [
        {0, 1, 2}, {3, 4, 5}, {6, 7, 8}, # Horizontal
        {0, 3, 6}, {1, 4, 7}, {2, 5, 8}, # Vertical
        {0, 4, 8}, # Main diagonal
        {2, 4, 6}, # Other diagonal
]
 
# Exploit the multi-line capabilities of Python to make it easier to understand
# what the data contains. The comments also point you directly to what the entries mean,
# while in your version you have to actually parse the entries to find the
# right one.
# Addedsomemorewhitespacetomakeiteasiertoread (here it's quite ok, but if you
# use floating point numbers, 0.0,0.0 versus 0,0,0,0 becomes hard to read. The
# comma behind the last entry makes it simpler to extend the list at a later
# time, though this list will never get extended, most likely.


    85	            else:
    86	                raise AssertionError("unexpected ui state")

    87	        #ai's second turn (human has made two moves)
    88	        elif player1move_count == 2:

# I would also insert empty lines between the various elif clauses, as I
# consider them separate cases.

 

19 hours ago, Aerodactyl55 said:

You say the AI is primitive but it is impossible to beat. Is that intentional? Maybe you can add a small chance for the AI to make a mistake, as an 'improvement'.

The goal was to make it extremely difficult. I said primitive because the algorithm I used (if you can call it that) is very simple. Seems to work quite effectively though. Go figure. I'll see I can add an "easy" setting.

Thanks for the code review Alberth! Will make the changes as I have time and push to GIT.

You know that tic-tac-toe is a solved problem, where the optimal strategy has been published?

https://en.wikipedia.org/wiki/Tictactoe#Strategy

 

 

To make it easier, draw a random number whether to play according to your strategy, or do a random move. By varying the treshold you can make it more random or more optimal.

Note that random moves may still be optimal, except it doesn't happen very often :P

 

This topic is closed to new replies.

Advertisement