I'm learning Python after a 30+ year break from programming, got bored with the usual TicTacToe exercise, so went a bit wild adding functionality to it to make something that's a bit more fun to play than losing to the computer every time.Now reached the stage of aimlessly noodling around with it, which suggests to me that fresh, and experienced, eyes would be handy. I would very much welcome a review of any/all aspects of the code. Be brutal - I can take it!A few points:It is written as a module, as I'm rubbish at UIs and I want to use this to practise with. Would it help if I also posted something that used the module? I have a fairly scraggy tkinter thing that I am using as a testbed if that would help.It works. Rather nicely I think - in fact I am quite proud of it. This of course is a Big Red Flag that a code review is needed ;)Nevertheless, it is possible that I have grossly abused something-or-other, but I've tried hard to get PEP8/PEP257 stuff right, and to comment lightly and selectively. So it shouldn't be too hard to work out what I am driving at. I hope.It's a bit big - runs to 8 pages printed, sorry about that.Module to support playable versions of TicTacToe (Noughts and Crosses)by phisheep 2017VARIABLES (all should be treated as read-only)    - USER, COMPUTER, EMPTY - Player objects    - cells[] - list of Cell objects representing the playing board    - lines[] - list of Line objects ditto    - moves[] - list of Cell objects representing moves made in current game    - game_over - boolean    - winning_line - Line object, or None    - winning_player - Player object, or NoneCLASSES    - Player() - One for each player, and EMPTY        player.symbol - arbitrary symbols for each player, default 'X', 'O' and ' '        player.opponent - the other (non-EMPTY) player    - Cell() - One for each cell on the board        cell.lines[] - list of Lines this cell is in        cell.set(player) - sets a cell and checks for win/loss/draw            - Line() - One for each row, column and diagonal on the board        line.cells[] - list of Cells in this line        line._value - SUM of the player values in this linePUBLIC FUNCTIONS     - reset_board() - clears the board for a new game     - available_cells() - returns list of available Cells     - board_list_symbols() - returns list of values on the board     - board_XY_symbols() - return 3x3 list of symbols on the board     - undo() - undoes moves to the previous USER move     - hints(player) - returns list of Cells that a good opponent                       might want to move in     - play(difficulty,player) - returns the Cell computer has moved in                                (after making the move!)INTERNAL FUNCTIONS    These fall into three groups:    - _play_...            one for each difficulty setting (returning a completed Cell, or None),                           which call...    - _make_...            to make a particular sort of move (returning a completed Cell, or None),                           which may call ...    - _list_possible_...   to list candidate moves (returning a list of Cells) ACKNOWLEDGEMENTS     Harvey, B (1997), Computer Science Logo Style: Volume 1 Symbolic Computing (2nd Edn),     MIT, Cambridge Mass     ISBN 0262581515 (set of 3 volumes)     ISBN 0262581485 (volume 1 only)     Chapter 6 at this link        https://people.eecs.berkeley.edu/~bh/pdf/v1ch06.pdf     ... for detailed discussion of tactics    import random_CENTRE = _EDGES = _CORNERS = tuple()cells = []lines = []moves = []game_over = Falsewinning_line = Nonewinning_player = Noneclass Player:     One instance for each of the two players USER and COMPUTER, also NULL     def __init__(self, value, symbol):        self._value = value        self.symbol = symbol    @property    def opponent(self):        if self == USER:            return COMPUTER        return USER# These assignments are here (rather than in __init__()) as they are used as default# arguments in function definitions further downUSER = Player(10, X)COMPUTER = Player(1, O)EMPTY = Player(0,  )class Cell:     One instance for each cell in the TicTacToe board.    ATTRIBUTES        - player - a Player object: USER, COMPUTER or EMPTY           - index - cell's position in cells[] list        - xpos, ypos - cell's position in x,y co-ordinates        - lines[] - Line objects, populated by Line() instance    FUNCTIONS        - set(value) - sets a cell value and end-game variables             def __init__(self, i):        self.index = i        self.player = EMPTY        self.xpos = self.index % 3        self.ypos = self.index // 3        self.lines = []    def set(self, player=USER):         Return a filled-in cell, having checked for end of game         global game_over, winning_line, winning_player        if game_over:            raise RuntimeError(Attempt to make a move when the game is over)        if player not in [USER, COMPUTER]:            raise ValueError(Cell must be set to either USER or COMPUTER)        self.player = player        moves.append(player)  # recorded in case of undo        for line in self.lines:            if line._value == 3 * self.player._value:                game_over = True                winning_line = line                winning_player = self.player                return self        if not available_cells():            game_over = True        return self    def _reset(self):        self.player = EMPTY        passclass Line:     One instance for each row, column and diagonal     ATTRIBUTES        - cells[] - list of Cells in this line        - value - (read-only) SUM of player values in cells in this line                    (used for testing line contents)        def __init__(self, *args):        self.cells = [cells[i] for i in args]        for cell in self.cells:            cell.lines.append(self)    @property    def _value(self):          Return total value of cells in this line.         return sum([cell.player._value for cell in self.cells])def __init__():     Initialise classes and module variables     # Warning: contains 'magic numbers' specific to this TicTacToe    # implementation. All gathered here so they don't clutter up    # the rest of the code.    #    # (... Except the Player objects, which had to be moved out    # because of getting default arguments right)    global _CENTRE, _EDGES, _CORNERS    for i in range(9):  # cells        cells.append(Cell(i))    for i in range(0, 9, 3):  # rows        lines.append(Line(i, i + 1, i + 2))    for i in range(3):  # columns        lines.append(Line(i, i + 3, i + 6))    lines.append(Line(0, 4, 8))  # diagonals    lines.append(Line(2, 4, 6))    _CENTRE = (cells[4],)    _EDGES = (cells[1], cells[3], cells[5], cells[7])    _CORNERS = (cells[0], cells[2], cells[6], cells[8])# PUBLIC FUNCTIONSdef reset_board():     Reset board to starting position.     global game_over, winning_line, winning_player    for cell in cells:        cell._reset()    moves.clear()    game_over = False    winning_line = None    winning_player = Nonedef available_cells():     Return list of empty cells.     return [cell for cell in cells if cell.player == EMPTY]def board_list_symbols():     Return list of all cell symbols.     return [cell.player.symbol for cell in cells]def board_XY_symbols():     Return 3x3 list of all cell symbols.     output = [] * 3    for cell in cells:        output[cell.ypos].append(cell.player.symbol)    return (output)def undo():     Undo moves back to last *user* move.     global game_over, winning_line, winning_player    if game_over:  # because it's not over any more!        game_over = False        winning_line = None        winning_player = None    if len(moves) != 0:        last_move = moves[-1]        if last_move.player == COMPUTER:            last_move._reset()            moves.pop()    if len(moves) != 0:        last_user_move = moves[-1]        last_user_move._reset()        moves.pop()def hints(player=USER):     Return list of possible winning moves by opponent.     return (_list_possible_wins_blocks(player.opponent) or            _list_possible_wins_blocks(player) or            _list_possible_forks(player.opponent) or            _list_possible_forcing_moves(player.opponent))def play(difficulty, player=COMPUTER):     Execute a playing strategy, returning the cell moved in.     # (This is a little scrappy and could do with tidying up)    #    move = [_play_loser, _play_random, _play_easy, _play_hard,            _play_handicap_2, _play_handicap_1, _play_expert]    if difficulty not in range(len(move)):        raise ValueError(No such difficulty setting -  + str(difficulty))    return move[difficulty](player)# PLAYING STRATEGIESdef _play_loser(player=COMPUTER):     Deliberately play to help opponent win.     #    # We use the hints() function to identify moves that a sane    # opponent would play, subtract those from the available    # cells, and move in any of the cells that remain. If    # none, take edges for preference.    #    candidates = set(available_cells())    candidates.difference_update(set(hints(player)))    if len(candidates) > 0:        cell = candidates.pop()        return cell.set(player)    return (_make_random_move(_EDGES, player) or            _play_random(player))def _play_random(player=COMPUTER):     Play in random available cell.     return _make_random_move(cells, player)def _play_easy(player=COMPUTER):     Complete winning line if possible, else play at random.     return (_make_win_or_block(player, player) or            _play_random(player))def _play_hard(player=COMPUTER):     Complete or block winning lines, prefer centre then corners.     return (_make_win_or_block(player, player) or            _make_win_or_block(player.opponent, player) or            _make_random_move(_CENTRE, player) or            _make_random_move(_CORNERS, player) or            _play_random(player))def _play_handicap_2(player=COMPUTER):     First two moves random, then expert     if len(moves) < 5:        return _play_random(player)    return _play_expert(player)def _play_handicap_1(player=COMPUTER):     First move to edge, then play as expert     if len(moves) < 3:        return _make_random_move(_EDGES, player)    return _play_expert(player)def _play_expert(player=COMPUTER):     Play expertly to not lose, the traditional 'perfect' TicTacToe     return (_make_win_or_block(player, player) or            _make_win_or_block(player.opponent, player) or            _make_forked_move(player) or            _make_forcing_move(player) or            _make_random_move(_CENTRE, player) or            _make_random_move(_CORNERS, player) or            _play_random(player))# Possible extensions:# - add play_cunning() to maximise winning chances against naive opponent# - add progressive play to adjust difficulty to user's skill level# FUNCTIONS - UTILITIES FOR COMPUTER MOVE STRATEGIESdef _make_random_move(target_cells, player=COMPUTER):     Return completed cell chosen from target_cells if available.     cells_list = []    for cell in target_cells:        if cell.player == EMPTY:            cells_list.append(cell)    if len(cells_list) != 0:        cell = random.choice(cells_list)        return cell.set(player)    return Nonedef _list_possible_wins_blocks(look_for_player):     Return list of cells with a winning move available.     possible_cells = []    for cell in available_cells():        for line in cell.lines:            if line._value == 2 * look_for_player._value + 0:                possible_cells.append(cell)    return possible_cellsdef _make_win_or_block(look_for_player, fill_with_player):     Return a completed cell that wins or blocks a line, or None     possible_cells = _list_possible_wins_blocks(look_for_player)    if len(possible_cells) != 0:        cell = random.choice(possible_cells)        return cell.set(fill_with_player)    return Nonedef _list_possible_forks(player=COMPUTER):     Return list of available forking moves.     #    # We're looking for two lines, each containing just one entry for    # 'player', that intersect in an empty cell. That's the cell we    # want to play in.    #    possible_forks = []    for cell in available_cells():        forks = 0        for line in cell.lines:            if line._value == player._value + 0 + 0:                forks += 1        if forks > 1:            possible_forks.append(cell)    return possible_forksdef _make_forked_move(player=COMPUTER):     Return completed forking move, or None.     my_forks = _list_possible_forks(player)    if len(my_forks) == 0:        return None    cell = random.choice(my_forks)    return cell.set(player)def _list_possible_forcing_moves(player=COMPUTER):     Find a forcing move that does not make opponent fork us.     #    # This one is a bit tricky. We are looking to force the opponent to    # block us, by having two cells in a line - if he doesn't block, we    # get to win next turn. BUT we must avoid 'forcing' him into a move    # that wins the game for him.    #    # So first we find what possible forking moves he has, then we look    # for our candidate lines and extract the two blank cells. If one    # of those cells is *not* a possible forking move for the opponent,    # we have to move in the *other* one.    #    # Got that? Good. Took me ages.    #    opponent_forks = _list_possible_forks(player.opponent)    my_moves = set()    for line in lines:        if line._value == player._value + 0 + 0:            temp = [cell for cell in line.cells if cell.player == EMPTY]            for i, cell in enumerate(temp):                if temp[i] not in opponent_forks:                    # move in the *other* empty cell on the line                    my_moves.add(temp[(i + 1) % 2])    return list(my_moves)def _make_forcing_move(player=COMPUTER):     Return completed forcing move, or None.     cell_list = _list_possible_forcing_moves(player)    if len(cell_list) == 0:        return None    cell = random.choice(cell_list)    return cell.set(player)## And, right down here, is the call to initialise the playing board.# As an old COBOL hand I can't get used to programming upside-down.#if __name__ != '__main__':    __init__()
TicTacToe (again!), but with difficulty levels, undo, and hints
python;beginner;python 3.x;tic tac toe