Python Forum
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Drawing a net of a cube
#11
Okay, great, but what if I want, let's say, to combine 3 or 4 in a row for a win?
3 in a row on one board or 4 in a row on combining 2 or more boards?
That's why I had an idea to write all the combos by hand (the code that I sent, your old code version).

Furthermore, I want all these squares to have same width and height, not to look like rectangles. I tried to add new variable "height" to constructor but there's obivously no height option in entry. Is it fixable with the grid somehow?
Reply
#12
Use images.
Reply
#13
How exactly should I use images? Because if you see the start of this thread, I tried to load images for boards and it didn't go well ...
Reply
#14
Did you look at all for how to use images in tkinter? You can put images in buttons and other tkinter widgets. When the button only contains an image, the default size of the button is set by the size of the image. To use images, Square would set the button image instead of the button text. Since the "X" and "O" images have their own color, you don't have to change the button color. Actually makes the program easier to write if you use images. I don't use images in my example because I want to post runnable code. You cannot run code that uses images unless you have the image files or make your own.

Using images, the Square looks like this:
class Square(tk.Button):
    """Special button for playing tic-tac-toe."""

    def __init__(self, parent, index):
        super().__init__(parent, image=images[None])
        self.index = index
        self._mark = None

    @property
    def mark(self):
        """What marker appears in square."""
        return self._mark

    @mark.setter
    def mark(self, mark):
        self._mark = mark
        self["image"] = images[mark]
This maks a dictionary mapping 'X', 'O', None to images
images = {}

class TicTacToe(tk.Tk):
    """A funky TicTacToe game.  Use map to make any shape board you want."""

    def __init__(self, map_):
        super().__init__()
        # Define images used by squares.
        image_dir = Path(__file__).parent
        images["X"] = tk.PhotoImage(file=image_dir / "x.png")
        images["O"] = tk.PhotoImage(file=image_dir / "o.png")
        images[None] = tk.PhotoImage(file=image_dir / "empty.png")

        self.title("Tic Tac Toe")
        self.player = "X"
        ...
Since you cannot (easily) change the color of the mark images, I modified the code to highlight the winner by changing the Square background color.

Now that you've seen how to make a tic-tac-toe board in tkinter you should have a good idea how you could do the same thing in pygame. You would have an X image and an O image. You would make a sprite for each square. On a mouse click you would check if the click was in any of the squares. If you clicked on a square you would play that square. When playing the square you would change the image of the Square/sprite to show the appropriate image. A lot of the code could be structured exactly the same as it is in tkinter. The main difference is pygame sprites don't have a click callback.
Reply
#15
Thank you!
Reply
#16
I want to change this 2-player game into a game against computer. I tried to implement this minimax algorithm that's in the first class in my code but I can't make it work. I tried a couple of options but I can't handle it.

I would appreciate some help!

Here's the old code version without my silly tries (XD) and class Robot with the minimax function that I'm trying to implement and adapt in the beginning of the code:

import tkinter as tk

#TRYING TO IMPLEMENT CLASS ROBOT

#START

class Robot:
    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)
    
#END
 
class Square(tk.Button):
    """Special button for playing tic-tac-toe."""
    colors = {"O": "blue", "X": "red"}
  
    def __init__(self, parent, index):
        super().__init__(parent, width=3, font=('Comic Sans MS', 20, 'bold'))
        self.index = index
        self._mark = None
  
    @property
    def mark(self):
        """What marker appears in square."""
        return self._mark
  
    @mark.setter
    def mark(self, mark):
        self._mark = mark
        self["fg"] = self.colors.get(mark, 'black')
        self["text"] = '' if mark is None else mark
  
  
class TicTacToe(tk.Tk):
    """Play tic-tac-toe.  Players take turn clicking on empty buttons
    to place their marker.  The first player to get three markers in
    a row, up/down, sideways or diagonally, wins
    """
    # All possible winning conbinations
    wins = ((0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8),
            (9, 10, 11), (12, 13, 14), (15, 16, 17), (9, 12, 15), (10, 13, 16), (11, 14, 17),
            (18, 19, 20), (21, 22, 23), (24, 25, 26), (18, 21, 24), (19, 22, 25), (20, 23, 26),
            (27, 28, 29), (30, 31, 32), (33, 34, 35), (27, 30, 33), (28, 31, 34), (29, 32, 35),
            (36, 37, 38), (39, 40, 41), (42, 43, 44), (36, 39, 42), (37, 40, 43), (38, 41, 44),
            (45, 46, 47), (48, 49, 50), (51, 52, 53), (45, 48, 51), (46, 49, 52), (47, 50, 53),

            (3, 6, 36, 39), (4, 7, 37, 40), (5, 8, 38, 41),
            (18, 21, 39, 42), (19, 22, 40, 43), (20, 23, 41, 44),
            (28, 29, 36, 37), (31, 32, 39, 40), (34, 35, 42, 43),
            (37, 38, 45, 46), (40, 41, 48, 49), (43, 44, 51, 52),
            (9, 12, 21, 24), (10, 13, 22, 25), (11, 14, 23, 26),

            (0, 3, 12, 15), (1, 4, 13, 16), (2, 5, 14, 17), 
            (27, 28, 46, 47), (30, 31, 49, 50), (33, 34, 52, 53),

            (0, 1, 27, 30), (3, 4, 28, 31), (6, 7, 29, 32),
            (1, 2, 47, 50), (4, 5, 46, 49), (7, 8, 45, 48),

            (18, 19, 32, 35), (21, 22, 31, 34), (24, 25, 30, 33),
            (19, 20, 48, 51), (22, 23, 49, 52), (25, 26, 50, 53),

            (15, 16, 27, 28), (12, 13, 30, 31), (9, 10, 33, 34),
            (16, 17, 46, 47), (13, 14, 49, 50), (10, 11, 52, 53),

            (6, 37, 41, 51), (7, 38, 48, 52), (7, 32, 34, 36), (8, 35, 37, 39),
            (19, 28, 32, 42), (20, 29, 39, 43), (18, 41, 43, 45), (19, 44, 46, 48),
            (19, 23, 32, 42), (19, 21, 44, 48)) #last one example of a diagonal combo over 2 boards
              
    def __init__(self):
        super().__init__()
        self.title('Tic Tac Toe')
         
        # create a frame to hold all four boards
        self.boards_frame = tk.Frame(self)
        self.boards_frame.pack(side="top", padx=5, pady=5)
         
        # create the left board
        self.frame1 = tk.Frame(self.boards_frame)
        self.frame1.pack(side="left")
        
        self.squares1 = []

        for i in range(9):
            self.squares1.append(Square(self.frame1, i))
        for square in self.squares1:
            row = square.index // 3
            column = square.index % 3         
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        self.frame1.pack(side="top")
        
        self.frame2 = tk.Frame(self.boards_frame)
        self.frame2.pack(side="left")

        for i in range(9, 18):
            self.squares1.append(Square(self.frame2, i))
        for square in self.squares1:        
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        self.frame3 = tk.Frame(self.boards_frame)
        self.frame3.pack(side="left")

        for i in range(18, 27):
            self.squares1.append(Square(self.frame3, i))
        for square in self.squares1:
            row = square.index // 3
            column = square.index % 3         
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        
        self.frame4 = tk.Frame(self.boards_frame)
        self.frame4.pack(side="left")

        for i in range(27, 36):
            self.squares1.append(Square(self.frame4, i))
        for square in self.squares1:
            row = square.index // 3
            column = square.index % 3      
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        
        self.frame5 = tk.Frame(self.boards_frame)
        self.frame5.pack(side="left")

        for i in range(36, 45):
            self.squares1.append(Square(self.frame5, i))
        for square in self.squares1:
            row = square.index // 3
            column = square.index % 3      
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        
        self.frame6 = tk.Frame(self.boards_frame)
        self.frame6.pack(side="left")

        for i in range(45, 54):
            self.squares1.append(Square(self.frame6, i))
        for square in self.squares1:
            row = square.index // 3
            column = square.index % 3
                 
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares1))

        self.frame2.pack(side="bottom")
        self.frame3.pack(side="bottom")
        
        '''
        # create the middle-left board
        self.frame2 = tk.Frame(self.boards_frame)
        self.frame2.pack(side="left")
        
        self.squares2 = [Square(self.frame2, i) for i in range(9)]
        for square in self.squares2:
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares2, self.squares1))

        
        # create the middle-right board
        self.frame3 = tk.Frame(self.boards_frame)
        self.frame3.pack(side="left")
         
        self.squares3 = [Square(self.frame3, i) for i in range(9)]
        for square in self.squares3:
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares3, self.squares1, self.squares2, self.squares4, self.squares5, self.squares6))

        self.frame4 = tk.Frame(self.boards_frame)
        self.frame4.pack(side="left")

        self.squares4 = [Square(self.frame4, i) for i in range(9)]
        for square in self.squares4:
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares4, self.squares1, self.squares2, self.squares3, self.squares5, self.squares6))

        self.frame5 = tk.Frame(self.boards_frame)
        self.frame5.pack(side="left")

        self.squares5 = [Square(self.frame5, i) for i in range(9)]
        for square in self.squares5:
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares5, self.squares1, self.squares2, self.squares3, self.squares4, self.squares6))

        self.frame1.pack(side="bottom")
        self.frame2.pack(side="top")
        
        self.frame6 = tk.Frame(self.boards_frame)
        self.frame6.pack(side="left")

        self.squares6 = [Square(self.frame6, i) for i in range(9)]
        for square in self.squares6:
            row = square.index // 3
            column = square.index % 3
            square.grid(row=row, column=column, padx=5, pady=5)
            square.configure(command=lambda arg=square: self.play(arg, self.squares6, self.squares1, self.squares2, self.squares3, self.squares4, self.squares5))

        self.frame3.pack(side="bottom")
        '''
            
        self.message = tk.Label(self, text=' ', width=30)
        self.message.pack(side="bottom")
         
        self.new_game = False
         
        self.player = "X"
        self.reset(self.squares1)
 
    def reset(self, squares1):
        """Reset board to empty"""
        self.open_squares = 54
        self.new_game = False
        for square in squares1:
            square.mark = None
        '''
        for square in squares2:
            square.mark = None
        for square in squares3:
            square.mark = None
        for square in squares4:
            square.mark = None
        for square in squares5:
            square.mark = None
        for square in squares6:
            square.mark = None
        '''
        self.message['text'] = f"{self.player}'s turn"
  
    def play(self, square, squares1):
        """Put player marker in slot if open.  Check for win or tie"""
        if self.new_game:
            return
        if square.mark is not None:
            self.message['text'] = "Invalid move"
            return
        square.mark = self.player
        self.open_squares -= 1
  
        # check for a win
        for win in self.wins:
            if all(squares1[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            '''
            if all(squares2[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            if all(squares3[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            if all(squares4[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            if all(squares5[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            if all(squares6[i].mark == self.player for i in win):
                self.message['text'] = f"{self.player} wins!"
                self.new_game = True
                return
            '''
        # check for a tie
        if self.open_squares == 0:
            self.message['text'] = "Tie game"
            self.new_game = True
            return
  
        # switch players
        self.player = "O" if self.player == "X" else "X"
        self.message['text'] = f"{self.player}'s turn"
 
    def game_over(self, squares1):
        """Check for winner or tie"""
        # Check all winning combinations
        for win in self.wins:
            if all(squares1[i].mark == self.player for i in win):
                for i in win:
                    squares1[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            '''
            if all(squares2[i].mark == self.player for i in win):
                for i in win:
                    squares2[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            if all(squares3[i].mark == self.player for i in win):
                for i in win:
                    squares3[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            if all(squares4[i].mark == self.player for i in win):
                for i in win:
                    squares4[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            if all(squares5[i].mark == self.player for i in win):
                for i in win:
                    squares5[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            if all(squares6[i].mark == self.player for i in win):
                for i in win:
                    squares6[i]["fg"] = "green"
                self.message["text"] = f"{self.player} wins!"
                return True
            '''
            
        # Check for a tie
        if self.open_squares <= 0:
            self.message["text"] = "Game ends in a tie"
            return True
 
        return False
 
 
TicTacToe().mainloop()
Reply
#17
On a tick tack toe board the maximum depth is 9 moves. On your board it is 54 moves. That should require a change in this line:
score = 10 - depth if mark is ROBOT else depth - 10
You don't want winning in 12 moves to be a negative score.

This doesn't really matter though. You cannot use minimax to play tic-tac-toe with 54 squares. Even a 4x4 board takes longer to make the first move than anyone is willing to wait. There are too many potential moves and it will take days (maybe weeks or months) for the computer to make the first move. Your only hope would be computing all winning combinations offline ad saving them to a database.

You need to try a different approach. What, I have no idea. Maybe you can use this to get an early start on your doctoral thesis.
Reply
#18
Yes I tried to adapt this line, but as you said, it doesn't really matter.

But if I only have winning combinations over one of the six boards, could I somehow implement my minimax algorithm (class Robot) into the code?

That was my biggest problem when drawing the surface (a net of a cube) because I didn't know how to draw my sample so I can use one board (9 squares) like I use all 6 boards (54 squares) like I used in the old game when I had a 3d-cube with 3 visible sides/boards by using pygame -> now I'm talking just about changing 3d-cube surface into a net of a cube that I have in the new version.

This was the old version that I'm describing in the last paragraph as you might remember:

"""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: 'dodger blue', PLAYER: 'green', ROBOT: 'red'}

 
class Robot:
    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("3D-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
#19
Only a part of the problem is due to the number of winning combinations. The main contributing factor to move computation time is the number of available moves.

The code below demonstrates this. It kind of plays out like minimax, if minimax didn't check if the last move won or lost. It just alternates between placing X's and O's in empty squares, counting up the number of plays until all possible moves have been made. It doesn't take long to complete all the different puzzles from size 1 to 9, but finishing 10 is a length delay. 11 takes a really long time, and I give up waiting for 12.
def combinations(num_squares):
    global count

    def walk(squares):
        global count
        for index, square in enumerate(squares):
            if square is None:
                squares[index] = 1
                count += 1
                walk(squares)
                squares[index] = None

    squares = [None] * num_squares
    count = 0
    walk(squares)
    return count


for i in range(1, 27):
    print(i, combinations(i))
Output:
1 1 2 4 3 15 4 64 5 325 6 1956 7 13699 8 109600 9 986409 10 9864100 11 108505111
This is why I used a heuristic in combination with minimax on the three face cube solver. Solving 3 tic-tac-toe games only takes three times longer than solving one, but solving a 27 square tic-tac-toe game takes well over a million times longer than solving a 9 square game.
Reply
#20
Yes makes sense. I got your point.

But can I somehow change this 3d-cube into a net of a cube by some slight changes in this code that uses pygame? I managed to make a net of a cube that I wanted with tkinter but it would be much easier if it's possible to continue using pygame and just change a shape.

This minimax works pretty fine and that's the reason why I would continue using pygame and just change a shape. But I don't know how to change it.

To repeat, I did it in tkinter but I don't know to do anything more using tkinter because as you said, there aren't 9 different squares on a board multiplied by 6, there are 54 different squares which makes the problem much harder to solve.

The code I just want to change the shape in:

"""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: 'dodger blue', PLAYER: 'green', ROBOT: 'red'}

 
class Robot:
    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:    #THIS IS WHAT I WANT TO CHANGE
    """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("3D-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


Possibly Related Threads…
Thread Author Replies Views Last Post
  Cube drawing freethrownucleus 51 10,274 Apr-12-2023, 08:04 AM
Last Post: ClaytonMorrison
  2D-Cube-Tic-Tac-Toe freethrownucleus 0 1,176 Mar-10-2023, 07:07 PM
Last Post: freethrownucleus
  PyGlet Trouble Drawing Cube. Windspar 3 5,770 Jan-02-2018, 06:37 PM
Last Post: Windspar

Forum Jump:

User Panel Messages

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