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

Pygame version of my 3D Tic Tac Toe/Connect 4

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

Problem

I posted a question a while back asking for some feedback on the code of a game I made (it was limited to typing the input and drawing the output in ASCII).

Now I've got it linked up with pygamef. Does anything look out of place? Do you notice any bugs? Do the colours work? Is there anything particularly annoying?

Use CTRL+SHIFT+d while in options (hit ESC to bring them up if you've already started the game) to reveal the debug settings, and enable to see the mouse coordinate conversion and AI stuff going on under the hood.

Instructions

The aim is to get as many complete rows as you can, and the grid will flip every 3 turns to throw you off, otherwise it gets a bit easy. The game ends when all spaces are taken (though this is a bit annoying when you are having to fill in the last few ones, so I'll just make it end when there are no points left).

At this time, I still need to make the instructions page and a 'player x won' page, though everything else is working without bugs as far as I can tell.

Normal game:

With debug enabled:

To see the entire thing, you'll need this link. If you don't have pygame (or python for that matter), here is a standalone version of the game from py2exe.

```
class MouseToBlockID(object):
"""Converts mouse coordinates into the games block ID.

The first part is to calculate which level has been clicked, which
then allows the code to treat the coordinates as level 0. From this
point, it finds the matching chunks from the new coordinates which
results in two possible blocks, then it calculates how they are
conected (highest one is to the left if even+odd, otherwise it's to
the right), and from that, it's possible to figure out which block
the cursor is over.

A chunk is a cell of a 2D grid overlaid over the isometric grid.
Each block is split into 4 chunks, and each chunk overlaps two
blocks.
"""

def __init__(self, x, y, grid_main):
self.x = x
self.y = y
sel

Solution


  1. MouseToBlockID



-
Normally an instance of a class represents some thing, that is, a persistent object or data structure. But an instance of MouseToBlockID does not seem to represent any kind of thing. What you need here is a function that takes game coordinates and returns a block index.

See Jack Diederich's talk "Stop Writing Classes".

Since this function makes use of the attributes of the GridDrawData class, this would best be written as a method on that class:

def game_to_block_index(self, gx, gy):
    """Return index of block at the game coordinates gx, gy, or None if
    there is no block at those coordinates."""


-
The naming of variables needs work. When you have coordinates in three dimensions, it's conventional to call them "x", "y" and "z". But here you use the name y_coordinate for "z". That's bound to lead to confusion.

-
The code is extraordinarily long and complex for what should be a simple operation. There are more than 200 lines in this class, but converting game coordinates to a block index should be a simple operation that proceeds as follows:

Adjust gy so that it is relative to the origin of the bottom plane (the z=0 plane) rather than relative to the centre of the window:

gy += self.centre


Find z:

z = int(gy // self.chunk_height)


Adjust gy so that it is relative to the origin of its z-plane:

gy -= z * self.chunk_height


Reverse the isometric grid transform:

dx = gx / self.size_x_sm
dy = gy / self.size_y_sm
x = int((dy - dx) // 2)
y = int((dy + dx) // 2)


Check that the result is in bounds, and encode position as block index:

n = self.segments
if 0 <= x < n and 0 <= y < n and 0 <= z < n:
    return n ** 3 - 1 - (x + n * (y + n * z))
else:
    return None


And that's it. Just twelve lines.

-
It will be handy to encapsulate the transformation from block coordinates to block index in its own method:

def block_index(self, x, y, z):
    """Return the block index corresponding to the block at x, y, z, or
    None if there is no block at those coordinates.

    """
    n = self.segments
    if 0 <= x < n and 0 <= y < n and 0 <= z < n:
        return n ** 3 - 1 - (x + n * (y + n * z))
    else:
        return None


See below for how this can be used to simplify the drawing code.

-
The encoding of block indexes is backwards, with (0, 0, 0) corresponding to block index 63 and (3, 3, 3) to block index 0. You'll see that had to write n ** 3 - 1 - (x + n (y + n z)) whereas x + n (y + n z) would be the more natural encoding.

  1. GridDrawData



-
The computation of game coordinates for the endpoints of the lines is verbose, hard to read, and hard to check:

self.line_coordinates = [((self.size_x, self.centre - self.size_y),
                          (self.size_x, self.size_y - self.centre)),
                         ((-self.size_x, self.centre - self.size_y),
                          (-self.size_x, self.size_y - self.centre)),
                         ((0, self.centre - self.size_y * 2),
                          (0, -self.centre))]


What you need is a method that transforms block coordinates into game coordinates:

def block_to_game(self, x, y, z):
    """Return the game coordinates corresponding to block x, y, z."""
    gx = (x - y) * self.size_x_sm
    gy = (x + y) * self.size_y_sm + z * self.chunk_height - self.centre
    return gx, gy


Then you can compute all the lines using block coordinates, which is much easier to read and check:

n = self.segments
g = self.block_to_game

self.lines = [(g(n, 0, n - 1), g(n, 0, 0)),
              (g(0, n, n - 1), g(0, n, 0)),
              (g(0, 0, n - 1), g(0, 0, 0))]

for i, j, k in itertools.product(range(n+1), range(n+1), range(n)):
    self.lines.extend([(g(i, 0, k), g(i, n, k)),
                       (g(0, j, k), g(n, j, k))])


-
Using block_to_game you can avoid the need for relative_coordinates. Instead of:

for i in self.C3DObject.range_data:
    if self.C3DObject.grid_data[i] != '':

        chunk = i / self.C3DObject.segments_squared
        coordinate = list(self.draw_data.relative_coordinates[i % self.C3DObject.segments_squared])
        coordinate[1] -= chunk * self.draw_data.chunk_height

        square = [coordinate,
                  (coordinate[0] + self.draw_data.size_x_sm,
                   coordinate[1] - self.draw_data.size_y_sm),
                  (coordinate[0],
                   coordinate[1] - self.draw_data.size_y_sm * 2),
                  (coordinate[0] - self.draw_data.size_x_sm,
                   coordinate[1] - self.draw_data.size_y_sm),
                  coordinate]


write:

```
n = self.draw_data.segments
g = self.draw_data.block_to_game
for x, y, z in itertools.product(range(n), repeat=3):
i = self.draw_data.block_index(x, y, z)
if self.C3DObject.grid_data[i] != '':
square = [g(x, y, z),
g(x + 1, y, z),
g(x + 1, y + 1, z),

Code Snippets

def game_to_block_index(self, gx, gy):
    """Return index of block at the game coordinates gx, gy, or None if
    there is no block at those coordinates."""
gy += self.centre
z = int(gy // self.chunk_height)
gy -= z * self.chunk_height
dx = gx / self.size_x_sm
dy = gy / self.size_y_sm
x = int((dy - dx) // 2)
y = int((dy + dx) // 2)

Context

StackExchange Code Review Q#108411, answer score: 5

Revisions (0)

No revisions yet.