HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

3D Tic Tac Toe/Connect Four game with AI

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
connecttoewithticfourtacgame

Problem

I came up with this during English lessons many years ago to kill some time, and just decided to try code it. It's basically a 3D version of Connect Four/Noughts and Crosses/Tic Tac Toe, but the grid can also flip itself, which means the people good at spacial awareness have a chance against people good at planning ahead.

Here's the general concept of how it works: it supposed to be 4x4x4 (coded to allow other sizes, though be warned an odd number gives player 1 a huge advantage), and you can get a point from making any complete row in any direction. The game ends once all available spaces are taken, and obviously the player with the most points wins.

A quick example with paint showing where the points are mid-game:

Anyway, I'm not sure how to do user interfaces yet, so it's literally just a printed output and a raw_input to choose where to go (works with coordinates or the cell ID, eg. first cell is (1,1,1) or 0), which admittedly doesn't make it very easy to play. Ideally it should be colour coded and you just click where to go.

You can run the game by using Connect3D().play(), though it'd be safer to do something like c3d = Connect3D, then c3d.play(), so you can resume a game if you quit it (for now, quit by hitting escape or cancel and it'll throw an error, I'll sort this out later).

The grid_data is stored as a 1D list where a bit of maths is used to calculate how to go each direction. To make sure it doesn't loop to the other side, the edge points of each direction are stored, so if any edge point is met, it knows it's reached the end of the line, and will look in the opposite direction. I know I haven't explained that well, but for example. with the above picture, for the direction up, the edges would be 0-15.

I'm aware a 3D list would have been much easier, but I quite liked the idea of storing it in a 1D list so that it'd be easy to save and recreate the grid with a single string.

Using the above picture, here's how the game would look

Solution

Style

Python has an official style guide, which you should read and follow. Some highlights:

-
Import statements should be one module per line, in alphabetical order:

from collections import defaultdict
import itertools
import operator
import random


-
Whitespace should be more consistent (e.g. a single blank line between method definitions, spaces after commas, spaces around operators).

-
Lines should be 80 characters or fewer in length (and docstrings should be

-
Function names should be lowercase_with_underscores (i.e. draw_grid not DrawGrid) - and again, consistency is important.

Documentation

Listing classes and functions (also methods, which you also refer to as functions) in the module docstring and methods in the class docstrings is:

  • Duplicated information; and



  • Redundant, as this basic data is available from e.g. dir(module) or dir(Class).



Instead, the docstrings should provide information that cannot simply be determined by introspection, like the purpose of the module/class; your method docstrings are pretty good, for example. I would be inclined to use an existing format rather than inventing my own, as this makes it easier to use tools like Sphinx to automatically generate HTML/PDF docs from the docstrings. I like the Google style:

def __init__(self, grid_size=4, _raw_data=None):
    """Set up the grid and which player goes first.

    Parameters:
      grid_size (int, optional): How long each side of the grid should
        be. The game works best with even numbers, 4 is the default and 
        recommended.

      _raw_data (str or None, optional): Passed in from ``__repr__``, 
        contains the grid data and current player. 
        Format: ``'joined(grid_data).current_player'``

    ...

    """


Having test examples in the docstrings is great; I'd be inclined to add:

if __name__ == '__main__':
    import doctest
    doctest.testmod()


at the end of the script to run them if the file script is executed directly.

Functionality

-
It's not clear why DirectionCalculation, PointConversion and SwapGridData are separate classes; all contain functionality that really belongs to Connect3D, although this would make it quite a long class.

-
Implementing DrawGrid as a separate function seems like a very odd choice; the fact that calling this is all that Connect3D.draw does is a bit of a red flag. I would have been inclined to make that the __str__ implementation, so you can simply print c3d rather than print c3d.draw().

-
All of the switching to and from strings seems redundant - pick a canonical representation of your data and stick with it.

-
It's odd to see a "private by convention" single-leading-underscore parameter like _raw_data. Is the user not supposed to provide it? In general, I would implement a from_string @classmethod to create a new instance from a string representation (see example below), rather than make it an optional __init__ argument.

-
Python is dynamically typed, but it still seems a bit odd for player_symbols to be "str/list/tuple/int". Really, you only care if it's an iterable of two characters, so you could do:

player_one, player_two = player_symbols  # ValueError if not two characters
if player_one == player_two:
    raise ValueError('symbols must be different')


If the user wants to pass in e.g. 12 for '1' and '2', let them do that conversion themselves before calling your method. Also, note that you calculate len(set(player_symbols)) == 1 twice, rather than reusing same_symbols.

How the from_string might work:

class Connect3D(object): 

    def __init__(self, grid_size):
        self.grid_data = ['' for i in range(pow(grid_size, 3))]
        self.current_player = 0
        ...  # carry out rest of grid_size-based setup

    @classmethod
    def from_string(cls, raw_data):
        grid_data, current_player = raw_data.split('.')
        grid_data = [i if i != ' ' else '' for i in grid_data]
        grid = cls(calculate_grid_size(grid_data))  # create new instance
        ...  # populate the instance with grid_data and current_player
        return grid  # return new instance


Now Connect3D.from_string(...) will create a new instance, populate it from the raw data and return it for use.

Code Snippets

from collections import defaultdict
import itertools
import operator
import random
def __init__(self, grid_size=4, _raw_data=None):
    """Set up the grid and which player goes first.

    Parameters:
      grid_size (int, optional): How long each side of the grid should
        be. The game works best with even numbers, 4 is the default and 
        recommended.

      _raw_data (str or None, optional): Passed in from ``__repr__``, 
        contains the grid data and current player. 
        Format: ``'joined(grid_data).current_player'``

    ...

    """
if __name__ == '__main__':
    import doctest
    doctest.testmod()
player_one, player_two = player_symbols  # ValueError if not two characters
if player_one == player_two:
    raise ValueError('symbols must be different')
class Connect3D(object): 

    def __init__(self, grid_size):
        self.grid_data = ['' for i in range(pow(grid_size, 3))]
        self.current_player = 0
        ...  # carry out rest of grid_size-based setup

    @classmethod
    def from_string(cls, raw_data):
        grid_data, current_player = raw_data.split('.')
        grid_data = [i if i != ' ' else '' for i in grid_data]
        grid = cls(calculate_grid_size(grid_data))  # create new instance
        ...  # populate the instance with grid_data and current_player
        return grid  # return new instance

Context

StackExchange Code Review Q#102585, answer score: 7

Revisions (0)

No revisions yet.