Lightweight Tic-Tac-Toe in Python This is a completely functional python script of the Tic-Tac-Toe game. I've used python's set container for almost all data structure and operation. The code is small and somewhat readable. I would very much appreciate your opinion and suggestions to improve it in terms of speed, readability and refactoring wherever necessary. Please post in your answer any modifications needed in the algorithm used.


The grid is marked from 1 to 9 horizontally.
The TTT class handles all the tasks.
self.ploys is a dictionary containing all the solution combinations.

{1: [{1,2,3}, {1,4,7}, {1,5,9}], 2:[{1,2,3}, {2,5,8}], ...}

The Main Loop displays the output, cycles through each player's turn and checks the result.
self.PlayUser and self.PlayCom plays respective turns.




#!/usr/bin/env python3


import os


erase = 'cls' if os.name == 'nt' else 'clear'
# 3x3 grid and 8 solution sets
grid = set(range(1,10))
sols = ({1,2,3}, {4,5,6},
        {7,8,9}, {1,4,7},
        {2,5,8}, {3,6,9},
        {1,5,9}, {3,5,7})


class TTT(object):

    def __init__(self):
        # Valid combinations (shortens during game)
        self.ploys = {k:[s for s in sols if k in s] for k in grid}
        # Holds two sets; user moves and com moves
        self.umoves = set()
        self.cmoves = set()


    def GameOver(self, moves):
        """Returns True if the puzzle is solved, False if it's a tie."""
        if any(s.issubset(moves) for s in sols):
            return True
        elif (self.umoves|self.cmoves) == grid:
            return False


    def PlayUser(self):
        """Plays the user's turn"""
        umove = int(input())
        # Filter invalid moves
        while umove not in grid or (umove in self.umoves or
            umove in self.cmoves):
            umove = int(input("Invalid Move "))
        self.umoves.add(umove)
        # Modify ploys
        del self.ploys[umove]
        for p, s in self.ploys.items():
            for i in [i for i,n in enumerate(s) if umove in n]:
                del self.ploys[p][i]

    def PlayCom(self, cmove=None, best=None):
        """Plays the com's turn"""
        for p, s in self.ploys.items():
            # Check if com can win
            if any(n.issubset({p}|self.cmoves) for n in s):
                cmove = p
                break
            # Check if user can win
            elif any(n.issubset({p}|self.umoves) for n in sols if p in n):
                cmove = p
            # Get the next best move
            elif best is None or len(s) &gt; len(self.ploys[best]):
                best = p
        cmove = cmove or best
        self.cmoves.add(cmove)
        del self.ploys[cmove]


    def Show(self, cols=3, com="X", usr="O", fill=" "):
        """Prints the grid in the command-line."""
        os.system(erase)
        for g in grid:
            if g in self.cmoves:
                print(com, fill, end="") if g % cols else print(com)
            elif g in self.umoves:
                print(usr, fill, end="") if g % cols else print(usr)
            else:
                # Replace g with fill for clean grid
                print(g, fill, end="") if g % cols else print(g)


    def Main(self):
        """Stars the game."""
        from itertools import cycle

        text = ("You Won", "You Lost")
        move = (self.umoves, self.cmoves)
        play = (self.PlayUser, self.PlayCom)

        for t, m, p in cycle(zip(text,move,play)):
            self.Show()
            p()
            self.Show()
            status = self.GameOver(m)
            if status is not None:
                print(t) if status is True else print("Game Over.")
                exit()


if __name__ == "__main__":
    game = TTT()
    game.Main()
The logic behind this section seems to be a little off.

for p, s in self.ploys.items():
    for i in [i for i,n in enumerate(s) if umove in n]:
        del self.ploys[p][i]


When you delete the value at self.ploys[p][i] this essentially means that:

self.ploys[p][i + 1] is now at position self.ploys[p][i]
self.ploys[p][i + 2] is now at position self.ploys[p][i + 1]
etc 


Because the comprehension in the second line finds the indices to the original array, all the proceeding deletes will delete the element 1 index ahead of their target, the next delete will be 2 ahead, and so on.

To fix this you can just iterate through the loop backwards.

for p, s in self.ploys.items():
    for i in [i for i,n in enumerate(s) if umove in n][::-1]:
        del self.ploys[p][i]


Along with that, I think it would help readability to give variables in your loop meaningful names.

It's much less cryptic as:

for ploy_index, soln_list in self.ploys.items():
    for soln_index in [i for i, soln in enumerate(soln_list)
                      if umove in soln][::-1]:
        del self.ploys[ploy_index][soln_index]




I notice is that in PlayUser you write the following.

while umove not in grid or (umove in self.umoves or
    umove in self.cmoves):
    ...


These brackets are a little confusing. When line length is a concern, you should put the entire expression in brackets or use the \ continuation. And although not explicitly in PEP8, I think it also looks better to add extra indentation to easily identify the condition from the logic.

while (umove not in grid or 
       umove in self.umoves or
       umove in self.cmoves):
    ...