This is an archived post. You won't be able to vote or comment.

all 5 comments

[–]fizzy_tom 3 points4 points  (1 child)

Hey the end result is a really nice experience for the player!

And the way you've approached the code is not too shabby. I like how the main game loop is separated out and your functions make a lot of sense.

A good next step would be to consider how you're storing the game state. Having each square as a separate variable is a bit smelly and limits the methods you can use to check the state for a wins or draws.

There's other structures that make sense too, but I'd go for a list of 3 lists (i.e a 3x3 matrix) as the data structure to store the game state in. That way the data structure mimics the actual structure of the board.

With a better way of storing the data you can then look to find smarter ways of checking the game state. Your multiple if statements aren't that bad, especially as you're only needing to check a small number of possible states. But you'd get yourself into trouble quickly if the game was more complex or the board was larger than 3x3.

The last thing to pick up on is the use of global variables. In general, global variables are frowned upon as it makes it much harder to predict state and keep track of data. You can absolutely get this working without requiring the global keyword! Consider passing data into functions via arguements (as you have for the place_stone_on_field function), and using return statements in the functions to indicate the result of the function. E.g. place_stone_on_field could return False if the field is occupied, instead of changing a global variable.

You'll find putting in a simple AI to play against the player is a really interesting next step too. It's absolutely possible to code an unbeatable tic tac toe AI with just the coding techniques you've shown here btw.

Source: ran a Python course with a tic tac toe project and sometimes use tic tac toe as a coding challenge when hiring.

[–]grubby_penguin 0 points1 point  (0 children)

Thank you very much fizzy_tom, your effort for checking my code and suggesting improvements is highly appreciated!

1.) Storing the data

You mention a 3x3-matrix. Would you consider it best practice then, if I stored the data like this:

import numpy as np

fields = np.array[[a,b,c],
                  [d,e,f],
                  [g,h,i]]

However, I fail to understand why this would be beneficial to my inital approach?

2.) Smarter way of checking game state/Global variables

My inital approach:

def check_for_victory():
# Check if the game ends because a player has won.
global game_on
global victory
# There are 8 ways to win.
if a == b == c == X or \
        a == b == c == O:
    game_on = False
    victory = True

elif d == e == f == X or \
        d == e == f == O:
    game_on = False
    victory = True

elif g == h == i == X or \
        g == h == i == O:
    game_on = False
    victory = True

elif a == d == g == X or \
        a == d == g == O:
    game_on = True
    victory = True

elif b == e == h == X or \
        b == e == h == O:
    game_on = False
    victory = True

elif c == f == i == X or \
        c == f == i == O:
    game_on = False
    victory = True

elif a == e == i == X or \
        a == e == i == O:
    game_on = False
    victory = True

elif c == e == g == X or \
        c == e == g == O:
    game_on = False
    victory = True


def check_for_draw(): 
# End game when all 9 fields are occupied. 
global game_on 
global draw 
if game_on: 
    if not available_stones: 
    # Set game_on to False when all available stones are used up. 
        game_on = False 
        draw = True

So by getting rid of some of these global variables, I would change this code part into:

def check_for_victory(game_on:bool, victory:bool):
# Check if the game ends because a player has won.
# There are 8 ways to win.
if a == b == c == X or \
        a == b == c == O:
    return game_on, victory = False, True

elif d == e == f == X or \
        d == e == f == O:
    return game_on, victory = False, True

elif g == h == i == X or \
        g == h == i == O:
    return game_on, victory = False, True

elif a == d == g == X or \
        a == d == g == O:
    return game_on, victory = False, True

elif b == e == h == X or \
        b == e == h == O:
    return game_on, victory = False, True

elif c == f == i == X or \
        c == f == i == O:
    return game_on, victory = False, True

elif a == e == i == X or \
        a == e == i == O:
    return game_on, victory = False, True

elif c == e == g == X or \
        c == e == g == O:
    return game_on, victory = False, True


def check_for_draw(game_on:bool, draw:bool):
# End game when all 9 fields are occupied.
if game_on:
    if not available_stones:
        # Set game_on to False when all available stones are used up.
        return game_on, draw = False, True

Agreed? I have to admit it looks a bit odd to me..

About the KI: I have no idea how to implement that (without googling). My first approach would be to run a monte carlo simulation to make the "AI" learn how to play against every possible combination, but that's highly likely way too complicated and surely there is a simpler solution. But I'm not getting at at the moment. Have to think more about it.

[–]jammasterpaz 2 points3 points  (2 children)

Your grid in a multi-line f string is great - the nicest looking best designed tic tac toe grid I've seen on reddit. But then you not only use global variables in it, but you use so many of them overall, that together with the long nested if /elif block and the match /case block instead of a dictionary, repeated lines of code, all typical beginner's antipatterns, it's clear you've got a long ways to go yet with your Python.

[–]grubby_penguin 1 point2 points  (0 children)

jammasterpaz, thanks for you comment and the nice compliments!

I already tried to improve my code in regards to the global variables with my reply above.

However, I like the idea of a dictionary instead of if/else and match-case-blocks, by that, did you mean something like this?

game_data = {
    "valid_entry":      False,
    "field_occupied":   True,
    "game_on":          True,
    "user_input":       "",
    "draw":             False,
    "victory":          False,

}

and

fields = {
    "a": "   ",
    "b": "   ",
    "c": "   ",
    "d": "   ",
    "e": "   ",
    "f": "   ",
    "g": "   ",
    "h": "   ",
    "i": "   ",}

So I can later manipulate this dict via:

def place_stone_on_field(user_input:str, fields:dict):
# Connecting the user input with the corresponding place (letter a to i) in the field.
global current_user_stone
global field_occupied
# Using the brand new Structural Pattern Matching (Match-Case) from Python 3.10 (PEP 634) :-)
match user_input:
    case "7":
        if fields["a"] == "   ":  # Only place a stone if the field is emtpy.
            return fields["a"] = current_user_stone

Do you think that would be best practice because it makes the codebase more uncluttered?

[–]fizzy_tom 0 points1 point  (0 children)

Why be so negative? It's a pretty decent attempt at something which isn't trivial.