python – TicTacToe object-oriented game with minimax algorithm

This program is a simple console-based tic tac toe game that uses minimax algorithm to get the best move for the computer. I have no major concerns and would appreciate a general review.

board.py

# board.py


class Board:

    def __init__(self):
        self.board_size = 3
        self.board_array = ( ( '.' for _ in range(self.board_size) ) for _ in range(self.board_size) )

    def draw(self):
        for i in range(len(self.board_array)):
            print('+------+------+------+')
            print('|      |      |      |')
            for j in range(len(self.board_array)):
                print('|  ', self.board_array(i)(j), end='  ')
            print('|')
            print('|      |      |      |')
        print('+------+------+------+')

    def is_move_valid(self, move_x, move_y):
        out_of_range = (move_x < 0 or move_x >= self.board_size) or (move_y < 0 or move_y >= self.board_size)

        if (not out_of_range) and (self.board_array(move_x)(move_y) == '.'):
            return True

        return False

    def check_victory(self):
        if self.__row_victory():
            return True
        if self.__column_victory():
            return True
        if self.__diagonal_victory():
            return True

        return False

    def __row_victory(self):
        for i in range(self.board_size):
            if (self.board_array(i)(0) != '.' and
                self.board_array(i)(0) == self.board_array(i)(1) == self.board_array(i)(2)):
                    return True

        return False

    def __column_victory(self):
        for i in range(self.board_size):
            if (self.board_array(0)(i) != '.' and
                self.board_array(0)(i) == self.board_array(1)(i) == self.board_array(2)(i)):
                    return True
        return False

    def __diagonal_victory(self):
        if (self.board_array(0)(0) != '.' and
            self.board_array(0)(0) == self.board_array(1)(1) == self.board_array(2)(2)):
            return True
        if (self.board_array(0)(2) != '.' and
            self.board_array(0)(2) == self.board_array(1)(1) == self.board_array(2)(0)):
            return True

        return False

    def stalemate(self):
        for i in range(self.board_size):
            for j in range(self.board_size):
                if self.board_array(i)(j) == '.':
                    return False

        return True

player.py

# player.py
from abc import ABC, abstractmethod
import enum

class Character(enum.Enum):
    naught = 1
    cross = 2

class AIMove:
    def __init__(self, x=0, y=0, score=0):
        self.x = x
        self.y = y
        self.score = score


class Player(ABC):

    character_dict = {Character.naught : 'O', Character.cross : 'X'}

    def __init__(self, player_character):
        self.player_character = player_character

    def __str__(self):
        return Player.character_dict(self.player_character)

    @abstractmethod
    def perform_move(self, board):
        pass


class HumanPlayer(Player):

    def __init__(self, player_character):
        super().__init__(player_character)

    def perform_move(self, board):
        try:
            location = int(input('Enter a location(0-8): '))
        except ValueError:
            print("Invalid location")
            return False
        
        move_x = location // board.board_size
        move_y = location % board.board_size
         
        if not board.is_move_valid(move_x, move_y):
            return False

        board.board_array(move_x)(move_y) = Player.character_dict(self.player_character)

        return True

class ComputerPlayer(Player):

    def __init__(self, player_character):
        super().__init__(player_character)

    def perform_move(self, board):
        if self.player_character == Character.naught:
           human_player = HumanPlayer(Character.cross)
        else:
            human_player = HumanPlayer(Character.naught)

        best_move = self.__get_best_move(board, self, human_player)
        board.board_array(best_move.x)(best_move.y) = Player.character_dict(self.player_character)

        return True

    def __get_best_move(self, board, current_player, prev_player):
        if board.check_victory():
            if prev_player == self:
                return AIMove(0, 0, 10)
            else:
                return AIMove(0, 0, -10)
        if board.stalemate():
            return AIMove()

        moves = ()

        for i in range(board.board_size):
            for j in range(board.board_size):
                if board.board_array(i)(j) == '.':
                    move = AIMove(i, j)
                    board.board_array(i)(j) = Player.character_dict(current_player.player_character)

                    if current_player == self:
                        move.score = self.__get_best_move(board, prev_player, self).score
                    else:
                        move.score = self.__get_best_move(board, self, current_player).score
                    
                    moves.append(move)
                    board.board_array(i)(j) = '.'
        
        best_move = 0
        if current_player == self:
            best_score = -1000000
            for i in range(len(moves)):
                if moves(i).score > best_score:
                    best_move = i
                    best_score = moves(i).score
        else:
            best_score = 1000000
            for i in range(len(moves)):
                if moves(i).score < best_score:
                    best_move = i
                    best_score = moves(i).score

        return moves(best_move)

tictactoe.py

# game.py

from os import name, system

from board import Board
from player import *

def clear():
    if name == 'nt':
        system('cls')
    else:
        system('clear')

class TicTacToe:
    
    def __init__(self):
        self.board = Board()
        self.player1 = HumanPlayer(Character.naught)
        print('1. Player vs Player')
        print('2. Player vs AI')
        choice = input()
        if choice == '1':
            self.player2 = HumanPlayer(Character.cross)
        elif choice == '2':
            self.player2 = ComputerPlayer(Character.cross)
        else:
            raise ValueError('Invalid choice')
        
        self.current_player = self.player1
        self.board.draw()

    def run(self):
        if self.current_player.perform_move(self.board):
            self.__toggle_turn()
        clear()
        self.board.draw()

    def game_over(self):
        if self.board.check_victory():
            self.__toggle_turn()
            print(f'{Player.character_dict(self.current_player.player_character)} has won the game')
            return True
        if self.board.stalemate():
            print('Stalemate.')
            return True

        return False

    def __toggle_turn(self):
        if self.current_player == self.player1:
            self.current_player = self.player2
        else:
            self.current_player = self.player1

main.py

# main.py
from tictactoe import TicTacToe

tictactoe_game = TicTacToe()

while not tictactoe_game.game_over():
    tictactoe_game.run()