Python Forum
Cube drawing for 3D Tic-Tac-Toe Python game
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Cube drawing for 3D Tic-Tac-Toe Python game
#1
Hi, I need some help.
I need to draw a cube (like this) [Image: 8HGHVN].
If anyone could help, I would really appreciate it! :)
A further idea is to play the game parallel on those 3 fields (cube sides).

Attached Files

Thumbnail(s)
   
Reply
#2
Drawing something like a rubic's cube does you no good. A 3D tic-tac-toe has 27 spots for x's and o's. An orthogonal view of a Rubic's cube only displays 19 cubes. Even if you spin it around you can't see the center.

Cube slides is no good either. There are 27 squares, but 8 of those are redundant, so you are only displaying 19 places where you can enter an x or o.

What you need are slices displaying the front face, the rear face, and the "center face".
Reply
#3
Hi, ty for replying!

Tbh, I didn't understand you well, except it's not a good idea.

But it's because I didn't explain what I need to do as understandable as I should.

To explain it the easiest way, my job is to do next:
there is a cube model that I've already posted and you should be playing just on those 3 sides (the exact cube and the way it's positioned) of the cube (classic Tic-Tac-Toe rules) without any rotation.

An example is here: https://gofile.io/d/La5dye (the cube shouldn't be positioned that way, it was recorded before I decided the cube's position).
Ok sorry, cannot post the example video because it's too large. :/ (EDIT: ignore this, an example's been posted!)
Reply
#4
So no 3D tic-tac-toe, just 3 regular tic-tac-toe games painted on the sides of a cube.
Reply
#5
Yeah ... sorry for a bad explanation / obviously a wrong title.
But, despite that, still need help.
Reply
#6
I wrote a 3D tic-tac-toe game with a very simple robot player. The code will look very familiar. I have not tested it much, so I wouldn't be surprised if some of the winning combinations are messed up.
"""3D tic-tac-toe game with robot player"""
import pygame
import random
from collections import Counter

background = "white"
EMPTY, PLAYER, ROBOT = ' ', 'X', 'O'  # board markers
colors = {EMPTY: 'dodger blue', PLAYER: 'red', ROBOT: 'black'}
winning_combos = (
    (0, 1, 2), (9, 10, 11), (18, 19, 20), (3, 4, 5), (12, 13, 14),
    (21, 22, 23), (6, 7, 8), (15, 16, 17), (24, 25, 26), (0, 3, 6),
    (9, 12, 15), (18, 21, 24), (1, 4, 7), (10, 13, 16), (19, 22, 25),
    (2, 5, 8), (11, 14, 17), (20, 23, 26), (0, 9, 18), (1, 10, 19),
    (2, 11, 20), (3, 12, 21), (4, 13, 22), (5, 14, 23), (6, 15, 24),
    (7, 16, 25), (8, 17, 26), (0, 4, 8), (9, 13, 17), (18, 22, 26),
    (2, 4, 6), (11, 13, 15), (20, 22, 24), (0, 12, 24), (1, 13, 25),
    (2, 14, 26), (6, 12, 18), (7, 13, 19), (8, 14, 20), (0, 10, 20),
    (3, 13, 23), (6, 16, 26), (2, 10, 18), (5, 13, 21), (8, 16, 24),
    (0, 13, 26), (2, 13, 24), (6, 13, 20), (8, 13, 20)
)
frequency = Counter(i for combo in winning_combos for i in combo)


class Robot:
    """Computer tic-tac-toe player.  Searches for for win or loss in one
    move, else selects random square
    """
    def __init__(self, board):
        self.board = board

    def play(self):
        """Return best square according to algorithm"""
        empty_squares = self.board.empty_squares()
        for square in empty_squares:
            if square.check_win(ROBOT):
                return square
        for square in empty_squares:
            if square.check_win(PLAYER):
                return square
        weights = [frequency[square.index] for square in empty_squares]
        square = random.choices(empty_squares, weights=weights, k=1)[0]
        return square


class Square:
    """Square in a 3D tic-tac-toe board"""
    def __init__(self, index, radius, center):
        self.radius = radius
        self.index = index
        self.mark = EMPTY
        self.center = center
        self.wins = []

    def rotate(self, rotation):
        """Rotate square (yaw, pitch, roll) degrees"""
        for deg, axis in zip(rotation, ((0, 0, 1), (0, 1, 0), (1, 0, 0))):
            if deg:
                self.center = self.center.rotate(deg, axis)
        return self

    def move(self, offset):
        """Move square offset (x, y, z) pixels"""
        self.center = self.center + offset
        return self

    def draw(self, surface):
        """Draw projection of square on xy plane"""
        pygame.draw.circle(
            surface, colors[self.mark], self.projection(), self.radius
        )

    def contains(self, point):
        """Return True if point inside square"""
        return self.projection().distance_to(point) <= self.radius

    def projection(self):
        """Return xy projection"""
        return pygame.Vector2(self.center.x, self.center.y)

    def check_win(self, mark=None):
        """Return winning combination involving square"""
        self.mark = self.mark if mark is None else mark
        for combo in self.wins:
            if all(self.mark == s.mark for s in combo):
                break
        else:
            combo = None
        self.mark = self.mark if mark is None else EMPTY
        return combo

    def add_win(self, squares):
        if self in squares:
            self.wins.append(squares)

class Board:
    """A 3D tic-tac-toe board"""
    def __init__(self, size, surface):
        spacing = size / 2
        radius = spacing / 8
        self.surface = surface
        self.squares = []
        for z in (-spacing, 0, spacing):
            for y in (-spacing, 0, spacing):
                for x in (-spacing, 0, spacing):
                    index = len(self.squares)
                    center = pygame.Vector3(x, y, z)
                    self.squares.append(Square(index, radius, center))
        for combo in winning_combos:
            squares = set(self.squares[i] for i in combo)
            for square in self.squares:
                square.add_win(squares)
        self.mark = PLAYER
        self.mark_count = 0
        self.winner = None

    def draw(self):
        """Draw board"""
        self.surface.fill(background)
        for i in range(9):
            a = self.squares[i].projection()
            b = self.squares[i+18].projection()
            pygame.draw.line(self.surface, colors[EMPTY], a, b)

        for i in range(0, 27, 3):
            a = self.squares[i].projection()
            b = self.squares[i+2].projection()
            pygame.draw.line(self.surface, colors[EMPTY], a, b)

        for square in self.squares:
            square.draw(self.surface)
        pygame.display.flip()

    def reset(self):
        """Ready board to play new game"""
        for square in self.squares:
            square.mark = EMPTY
        self.mark_count = 0
        self.winner = None

    def done(self):
        """Return winning squares if winner, True if draw, False if not done"""
        return self.winner or self.mark_count >= 27

    def empty_squares(self):
        """Return list of empty squares"""
        return [s for s in self.squares if s.mark is EMPTY]

    def click(self, point):
        """Try to place player's mark in clicked square."""
        if self.mark is PLAYER:
            for square in self.squares:
                if square.contains(point):
                    if square.mark is EMPTY:
                        self.play(square, self.mark)
                        return square
                    break
        return None

    def play(self, square, mark):
        """Place mark in square.  Test if is a winning move"""
        square.mark = mark
        self.mark_count += 1
        self.winner = square.check_win()
        if self.winner:
            for square in self.squares:
                if square not in self.winner:
                    square.mark = EMPTY
        self.mark = PLAYER if mark is ROBOT else ROBOT


def main():
    """Play tic-tac-toe"""
    pygame.display.set_caption("Tic-Tac-Toe")
    surface = pygame.display.set_mode((500, 500))
    center = pygame.Vector3(250, 250, 0)

    # Move cube for pretty orthogonal view at center of screen
    board = Board(300, surface)
    robot = Robot(board)
    for square in board.squares:
        square.rotate((0, 35, 12)).move(center)
    board.draw()

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if board.done():
                    board.reset()
                else:
                    board.click(pygame.Vector2(event.pos))
                if board.mark == ROBOT and not board.done():
                    board.draw()
                    board.play(robot.play(), ROBOT)
                board.draw()


if __name__ == "__main__":
    pygame.init()
    main()
    pygame.quit()
Reply
#7
Hi, thanks!

We've already talked to each other and I was wondering if I can somehow change the minimax algorithm so that the computer tries to score 3 in a row (in a line) when trying to win and then tries to score the other 2 combos. You can see the winning combos from the code.

"""Tic-tac-toe with a computer opponent.  Three boards played simultaneously"""
import pygame
import random
 
PLAYER = 'X'
ROBOT = 'O'
EMPTY = ' '
background = "aliceblue"
msg_color = "dodgerblue"
colors = {EMPTY: 'dodgerblue', PLAYER: 'green', ROBOT: 'red'}

 
class Robot:
    """Computer tic-tac-toe player.  Uses minimax algorithm with
    alpha/beta pruning and some additional heuristics to provide
    a challenging, but beatable opponent.
    """
    def __init__(self, cube):
        self.boards = cube.boards
 
    def get_scores(self, board):
        """Get scores for all open squares on a board"""
 
        def minimax(mark, square, alpha=-1000, beta=1000, depth=0):
            """minimax algorithm with alpha/beta pruning"""
            # Place the mark and check for a win
            square.mark = mark
            if square.check_win():
                # Give extra weight to earlier wins/losses
                score = 10 - depth if mark is ROBOT else depth - 10
                board.depth = min(board.depth, depth)
            elif len(empty_squares := board.empty_squares()) == 0:
                # No plays left.  Draw.
                score = 0
            elif mark is PLAYER:
                # Pick best move for robot
                score = -1000
                for s in empty_squares:
                    score = max(score, minimax(ROBOT, s, alpha, beta, depth+1))
                    alpha = max(alpha, score)
                    if alpha > beta:
                        break
            else:
                # Guess what move player will make
                score = 1000
                for s in empty_squares:
                    score = min(score, minimax(PLAYER, s, alpha, beta, depth+1))
                    beta = min(beta, score)
                    if alpha > beta:
                        break
 
            # Remove mark and return score for the square
            square.mark = EMPTY
            return score
 
        # Collect scores for empty squares.  If board is empty,
        # minimax will return 0 for all squares
        board.depth = 10
        empty_squares = board.empty_squares()
        if len(empty_squares) == 9:
            board.scores = [[0, s] for s in empty_squares]
        else:
            # Calling minimax twice.  The first is to find the square
            # giving us best chance to win board.  Second is to prevent
            # abandoning board early and giving player an easy win.
            board.scores = [[minimax("O", s), s] for s in empty_squares]
            [minimax("X", s, depth=1) for s in empty_squares]
 
    def play(self):
        """Place robot mark."""
        # Get scores for all empty squares
        depth = 10
        for board in self.boards:
            self.get_scores(board)
            depth = min(depth, board.depth)
 
        # Select board with minimum depth.  This is the board that
        # will win/lose in the least number of moves.
        scores = []
        for board in self.boards:
            if board.depth <= depth:
                scores.extend(board.scores)
 
        # Randomly select from best scores on the selected board.
        max_score = max(score[0] for score in scores)
        squares = [score[1] for score in scores if score[0] >= max_score]
        return random.choice(squares)
 
 
class Square:
    """A 3D square in a tic-tac-toe board"""
    def __init__(self, board, index, side, center=(0, 0, 0)):
        self.board = board
        self.index = index
        self.mark = EMPTY
        self.corners = [
            pygame.Vector3(-1, -1, 0) * side / 2,
            pygame.Vector3(1, -1, 0) * side / 2,
            pygame.Vector3(1, 1, 0) * side / 2,
            pygame.Vector3(-1, 1, 0) * side / 2
        ]
        self.move(center)
 
    def rotate(self, rotation):
        """Rotate square (rx, ry, rz) degrees"""
        for deg, axis in zip(rotation, ((1, 0, 0), (0, 1, 0), (0, 0, 1))):
            if deg:
                for corner in self.corners:
                    corner.rotate_ip(deg, axis)
        return self
 
    def move(self, offset):
        """Move square offset (x, y, z) pixels"""
        x, y, z = offset
        for corner in self.corners:
            corner.x += x
            corner.y += y
            corner.z += z
        return self
 
    def projection(self):
        """Return corners projected on xy plane"""
        return [pygame.Vector2(p.x, p.y) for p in self.corners]
 
    def draw(self, surface):
        """Draw projection of square on xy plane"""
        pygame.draw.polygon(surface, colors[self.mark], self.projection())
 
    def contains(self, point):
        """Return True if projection contains point."""
        def area(a, b, c):
            """Compute area of triangle"""
            return abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) / 2.0)
 
        def has_point(a, b, c, p):
            """Return True if triange ABC contains point p."""
            return area(a, b, c) >= int(area(a, p, b) + area(b, p, c) + area(c, p, a))
 
        a, b, c, d = self.projection()
        return has_point(a, b, c, point) or has_point(a, c, d, point)
 
    def check_win(self):
        return self.board.check_win(self)
 
 
class Board:
    """A tic-tac-toe board"""
    winning_combos = (
        (0, 2, 4, 6, 8), (1, 3, 5, 7), (0, 1, 2), (3, 4, 5),
        (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), 
    )
 
    def __init__(self, side):
        dx = side / 3
        self.squares = []
        for index in range(9):
            x = (index % 3 - 1) * dx
            y = (index // 3 - 1) * dx
            self.squares.append(Square(self, index, dx - 3, (x, y, 0)))
        self.scores = None
        self.depth = 10
 
    def empty_squares(self):
        """Return list of empty squares"""
        return [s for s in self.squares if s.mark is EMPTY]
 
    def check_win(self, square):
        """Return winning squares from last move, else None"""
        for combo in self.winning_combos:
            if square.index in combo:
                squares = [self.squares[i] for i in combo]
                if all(square.mark == s.mark for s in squares):
                    return squares
        return None
 
 
class Cube:
    """Three tic-tac-toe boards plastered on the faces of a cube"""
    def __init__(self, side):
        def make_board(center, rotation):
            board = Board(side)
            for square in board.squares:
                square.rotate(rotation).move(center)
            return board
 
        self.boards = [
            make_board((0, 0, -side/2), (0, 0, 0)),
            make_board((side/2, 0, 0), (0, 270, 0)),
            make_board((0, -side/2, 0), (90, 0, 0))
        ]
        self.squares = [s for b in self.boards for s in b.squares]
        self.winner = None
        self.mark = PLAYER
        self.reset()
 
    def click(self, point):
        """Try to place player's mark in clicked square."""
        if self.mark is PLAYER:
            for square in self.squares:
                if square.contains(point):
                    if square.mark is EMPTY:
                        self.play(square, self.mark)
                        return square
                    break
        return None
 
    def play(self, square, mark):
        """Place mark in square.  Test if is a winning move"""
        square.mark = mark
        self.mark_count += 1
        if winner := square.check_win():
            self.winner = winner
            for square in self.squares:
                if square not in winner:
                    square.mark = EMPTY
        self.mark = PLAYER if mark is ROBOT else ROBOT
 
    def done(self):
        """Return True if there is a winner or draw"""
        return self.winner or self.mark_count >= 27
 
    def reset(self):
        """Reset all boards to empty"""
        self.mark_count = 0
        self.winner = None
        for square in self.squares:
            square.mark = EMPTY
 
 
def main():
    """Play tic-tac-toe"""
    def blit_text(surface, msg, pos, font, color = pygame.Color('dodgerblue')):
        x, y = pos
        m = msg.split('\n')
        for line in m:
            text = pygame.font.Font(None, 48).render(line, True, msg_color)
            width, height = text.get_size()
            surface.blit(text, (x - width/2, y - height*2))
            y += height + 2.
 
    def refresh_screen(surface, msg = None):
        """Draw the cube and an optional message"""
        surface.fill(background)
        for square in cube.squares:
            square.draw(surface)
        text_player1 = pygame.font.Font(None, 48).render("You", True, 'green')
        text_vs = pygame.font.Font(None, 48).render("vs", True, 'dodgerblue')
        text_player2 = pygame.font.Font(None, 48).render("Computer", True, 'red')
        surface.blit(text_player1, (text_player1.get_width(), text_player1.get_height()))
        surface.blit(text_vs, (center.x - text_vs.get_width() / 2, text_vs.get_height()))
        surface.blit(text_player2, (center.x + text_player2.get_width(), text_player2.get_height()))
        if msg:
            text = pygame.font.Font(None, 48).render(msg, True, msg_color)
            pos = (350, 700)
            blit_text(surface, msg, pos, None, color = pygame.Color('dodgerblue'))
        pygame.display.flip()

    pygame.display.set_caption("Tic-Tac-Toe")
    surface = pygame.display.set_mode((700, 750))
    center = pygame.Vector3(350, 350, 0)
 
    # Move cube for pretty orthogonal view at center of screen
    cube = Cube(300)
    for square in cube.squares:
        square.rotate((0, 45, 0)).rotate((30, 0, 0)).move(center)
    robot = Robot(cube)
    refresh_screen(surface, None)
 
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if cube.done():
                    # Start a new game
                    cube.reset()
                    if cube.mark is ROBOT:
                        cube.play(robot.play(), ROBOT)
                    refresh_screen(surface, None)
                elif cube.click(pygame.Vector2(event.pos)):
                    # Player selected square.  Robot's turn,
                    if not cube.done():
                        refresh_screen(surface, None)
                        cube.play(robot.play(), ROBOT)
                    # Is the game over?
                    if cube.done():
                        refresh_screen(surface, "Ready for another play?\nJust click on the screen!")
                    else:
                        refresh_screen(surface, None)
                    pygame.event.clear()
 
 
if __name__ == "__main__":
    pygame.init()
    main()
    pygame.quit()
Reply
#8
Stop replaying to posts each time you ask a question. It makes the threads really long. Your last post had nothing to do with my last post. There is no reason why my post should be included. Imagine what a mess it would be if I replied to your post, then you replied to mine, and I replied back and so on an so on. Break the chain!

Minimax makes all decisions based on scores. If you prefer than minimax tries to win using rows and columns, modify the game code so that check_win returns a score. If the score is higher for wins that are rows and columns, minimax will give preference to plays that complete rows or columns.

Not sure how you would implement the scoring. Maybe multiple lists of winning combinations?
# Not real code
]rows = [(0, 1, 2), (3, 4, 5), (6, 7, 8)]
columns = [(0, 3, 6), (1, 4, 7), (2, 5, 8)]
diagonals = [(0, 4, 8), (2, 4, 6)]

if win_in_combos(columns):
    score = 4
elif win_in_combos(rows):
    score = 3
elif win_in_combos(diagonals):
    score = 2
Or maybe encode the score in each of the combinations. Maybe the first value
# Not real code
winning_combos = [(4, (0, 1, 2)), (3, (0, 3, 6)), (2, (0, 4, 8))]

for combo in winning_combos:
    score, squares = combo:
    if combo_wins(squares):
        return score
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020