Created
August 9, 2020 18:42
-
-
Save ygrenzinger/fd68dc3242884781a0543ee07bd568f9 to your computer and use it in GitHub Desktop.
Tic Tac Toe
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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