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()