Skip to content

Instantly share code, notes, and snippets.

@ygrenzinger
Created August 9, 2020 18:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ygrenzinger/fd68dc3242884781a0543ee07bd568f9 to your computer and use it in GitHub Desktop.
Save ygrenzinger/fd68dc3242884781a0543ee07bd568f9 to your computer and use it in GitHub Desktop.
Tic Tac Toe
# more info here https://github.com/Cledersonbc/tic-tac-toe-minimax
import random
from enum import Enum
from math import inf as infinity
class PlayerType(Enum):
USER = 1
EASY = 2
MEDIUM = 3
HARD = 4
@classmethod
def parse(cls, s):
if s == "user":
return PlayerType.USER
elif s == "easy":
return PlayerType.EASY
elif s == "medium":
return PlayerType.MEDIUM
elif s == "hard":
return PlayerType.HARD
else:
raise ValueError('Impossible to parse player type.')
class TicTacToe:
winning_positions = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
def __init__(self, x, o):
if not isinstance(x, PlayerType) or not isinstance(o, PlayerType):
raise ValueError('Wrong player input.')
self.x = x
self.o = o
self.cells = list(' ' * 9)
self.active_player = 'X'
def game_loop(self):
self.print_cells()
while not self.has_end():
if self.active_player == 'X':
pos = self.selected_position(self.x)
else:
pos = self.selected_position(self.o)
self.change_cell(pos)
self.print_cells()
self.switch_player()
def print_cells(self):
print("---------")
print("| " + self.str_row(self.cells[:3]) + " |")
print("| " + self.str_row(self.cells[3:6]) + " |")
print("| " + self.str_row(self.cells[6:9]) + " |")
print("---------")
def change_cell(self, index):
self.cells[index] = self.active_player
def switch_player(self):
self.active_player = self.opponent(self.active_player)
@classmethod
def opponent(cls, player):
if player == 'X':
return 'O'
else:
return 'X'
def selected_position(self, actor):
if actor == PlayerType.EASY:
return self.easy_ai()
elif actor == PlayerType.MEDIUM:
return self.medium_ai()
elif actor == PlayerType.HARD:
return self.hard_ai()
else:
return self.enter_coordinate()
def easy_ai(self):
print('Making move level "easy"')
return random.choice(self.empty_positions(self.cells))
@classmethod
def empty_positions(cls, cells):
return [x for x in range(0, 9) if cells[x] == ' ']
def medium_ai(self):
print('Making move level "medium"')
winning_pos = self.winning_pos_for_player(self.active_player)
if winning_pos is not None:
return winning_pos
loosing_pos = self.winning_pos_for_player(self.opponent(self.active_player))
if loosing_pos is not None:
return loosing_pos
return random.choice(self.empty_positions(self.cells))
def hard_ai(self):
print('Making move level "hard"')
depth = len(self.empty_positions(self.cells))
pos = self.minimax(self.cells, depth, self.active_player)
return pos[0]
@classmethod
def minimax(cls, cells, depth, player):
if player == 'X':
best = [-1, -infinity]
else:
best = [-1, +infinity]
player_win = cls.has_win(cells, player)
opponent_win = cls.has_win(cells, cls.opponent(player))
if depth == 0 or player_win or opponent_win:
score = 0
if player_win:
score = 1
elif opponent_win:
score = -1
return [-1, score]
for pos in cls.empty_positions(cells):
cells[pos] = player
score = cls.minimax(cells, depth - 1, player)
cells[pos] = ' '
score[0] = pos
if player == 'X':
if score[1] > best[1]:
best = score
else:
if score[1] < best[1]:
best = score
return best
def winning_pos_for_player(self, player):
for row in self.winning_positions:
active_player_pos = [pos for pos in row if self.cells[pos] == player]
count = len(active_player_pos)
if count == 2:
return set(row).difference(set(active_player_pos)).pop()
return None
def enter_coordinate(self):
def valid_pos(input_pos):
try:
inputs = [int(x) for x in input_pos.split()]
inputs = [x for x in inputs if 0 < x < 4]
if len(inputs) != 2:
print("Coordinates should be from 1 to 3!")
return -1
else:
index = self.convert_to_index(inputs)
if self.cells[index] == ' ':
return index
else:
print("This cell is occupied! Choose another one!")
return -1
except ValueError:
print("You should enter numbers!")
return -1
pos = valid_pos(input("Enter the coordinates: "))
while pos == -1:
pos = valid_pos(input("Enter the coordinates: "))
return pos
def has_end(self):
if self.has_win(self.cells, 'X'):
print("X wins")
return True
elif self.has_win(self.cells, 'O'):
print("O wins")
return True
elif self.is_draw(self.cells):
print("Draw")
return True
else:
return False
@classmethod
def is_draw(cls, cells):
return len(cls.remove_blank(cells)) == 9
@classmethod
def has_win(cls, cells, player):
rows = []
i = 0
while i < 9:
rows.append(cells[i:i + 3])
i += 3
for check in cls.winning_positions:
result = all([cells[i] == player for i in check])
if result:
return True
return False
def is_valid(self):
counter = {
'X': 0,
'O': 0
}
remaining = self.remove_blank(self.cells)
while remaining:
c = remaining[0]
count, remaining = self.take_and_count_while(remaining, c)
if count > 3:
return False, ''
counter[c] = counter[c] + count
if abs(counter['X'] - counter['O']) > 1:
return False, ''
if (counter['X'] - counter['O']) == 1:
return True, 'O'
else:
return True, 'X'
@classmethod
def convert_to_index(cls, pos):
(i, j) = pos
return (abs(j - 3) * 3) + (i - 1)
@classmethod
def take_and_count_while(cls, cells, symbol):
i = 0
while i < len(cells) and cells[i] == symbol:
i += 1
return i, cells[i:]
@classmethod
def str_row(cls, row):
return ' '.join(list(row))
@classmethod
def remove_blank(cls, cells):
return [c for c in cells if c != ' ']
def start_game(command):
elmts = command.split()
if elmts[0] != "start" or len(elmts) != 3:
raise ValueError("Unknown command.")
x = PlayerType.parse(elmts[1])
o = PlayerType.parse(elmts[2])
game = TicTacToe(x, o)
game.game_loop()
command = input("Input command: ")
while command != 'exit':
try:
start_game(command)
except ValueError:
print("Bad parameters!")
command = input("Input command: ")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment