Skip to content

Instantly share code, notes, and snippets.

@bendavis78
Created March 15, 2019 17:47
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 bendavis78/563af6186e79870debd6f68b4a6a780c to your computer and use it in GitHub Desktop.
Save bendavis78/563af6186e79870debd6f68b4a6a780c to your computer and use it in GitHub Desktop.
import ml4k
import random
import tkinter as tk
from copy import copy
from tkinter import messagebox
API_KEY = "PASTE-API-KEY-HERE"
AI_MODE = "random"
TRAINING = False
# The board state can be represented as a list with 9 items, one for each space on the game board.
# We'll pick some arbitrary numbers to represent how each space is occupied:
#
# 0: Unoccupied space
# 1: Occupied by the player
# -1: Occupied by the opponent
#
# The board starts out as empty, so we set each item to 0.
board = [
0, 0, 0,
0, 0, 0,
0, 0, 0
]
labels = [
'top_left', 'top_middle', 'top_right',
'middle_left', 'middle_middle', 'middle_right',
'bottom_left', 'bottom_middle', 'bottom_right'
]
player_moves = []
# We'll also store the buttons in a similiar way. This list will be auto-populated by make_button()
buttons = []
#
# Game Window Setup
#
# Now we create our window and give it a title. We'll store this window in a variable called
# "game", so we can access it later.
game = tk.Tk()
game.title("Tic Tac Toe")
# Add a message area (to display text to the user). Use `message.set(text)` to change the message.
message = tk.StringVar()
message_area = tk.Label(game, textvariable=message, font="Times 20 bold", pady=20)
message_area.grid(row=0)
# Add a "holder" for our buttons
button_holder = tk.Frame(game)
button_holder.grid(row=1)
#
# Function Definitions:
#
def check_victory(space_index):
"""
Returns `True` if there's a winner based on the last-played postion, otherwise returns `False`
"""
# First, we need to get the row and column from the space_index:
# To learn more about the // and % operators, see here:
# https://docs.python.org/3/reference/expressions.html#binary-arithmetic-operations
row = space_index // 3
col = space_index % 3
# check if previous move caused a win on vertical line
if board[col] == board[3 + col] == board[6 + col]:
return True
# check if previous move caused a win on horizontal line
if board[3 * row] == board[3 * row + 1] == board[3 * row + 2]:
return True
# check if previous move was on the main diagonal and caused a win
if row == col and board[0] == board[4] == board[8]:
return True
# check if previous move was on the secondary diagonal and caused a win
if row + col == 2 and board[2] == board[4] == board[6]:
return True
return False
def make_move(space_index):
"""
Makes a move for the current player (whether human or computer).
This function returns `True` if the move results in a win, otherwise it returns `False`.
"""
if game.current_player == "X":
player_moves.append([
space_index, copy(board)
])
# Set the space to the current player (1 for X, or -1 for O)
if game.current_player == "X":
board[space_index] = 1
else:
board[space_index] = -1
# Change the button text to the current player (X or O)
buttons[space_index].configure(text=game.current_player)
# If there's a winner, show a message and start a new game
if check_victory(space_index):
messagebox.showinfo("Game over!", "The winner is: " + game.current_player + "!")
if game.current_player == "X" and TRAINING:
train_winning_moves()
new_game()
return True
# Else if there are no spaces are left empty, it's a draw!
elif 0 not in board:
messagebox.showinfo("Game over!", "It's a draw!")
new_game()
return True
# Otherwise, switch players
else:
if game.current_player == "X":
game.current_player = "O"
else:
game.current_player = "X"
message.set(game.current_player + "'s move")
return False
def on_click(event):
"""
This function gets called any time a button is clicked. Each button has a `row` and `col`
attribute, which we can pass to the `make_move()` function.
"""
clicked_button = event.widget
# We can use the .index() method to find the position of an item within a list
space_index = buttons.index(clicked_button)
# If space is already occupied or it's not our turn, don't do anything
if board[space_index] != 0 or (AI_MODE and game.current_player != "X"):
return
# The `make_move()` function returns `True` if it results in a win.
# We store that result in a variable so we can use it in the next step.
is_winning_move = make_move(space_index)
if not is_winning_move:
# Wait 1 second before calling `ai_move()` to make it look like the computer is "thinking".
game.after(1000, ai_move)
def new_game():
"""
Clears the game board and starts a new game
"""
# re-set the buttons list and player moves
buttons.clear()
player_moves.clear()
# Remove existing button widgets
for widget in button_holder.winfo_children():
widget.destroy()
# Fill the window with new buttons
for space_index in range(0, 9):
# Make the button and add it to our buttons list
button = make_button(space_index)
buttons.append(button)
# Set the board space to 0 (empty)
board[space_index] = 0
# Choose a random player
game.current_player = random.choice(["X", "O"])
message.set(game.current_player + "'s move")
if game.current_player == "O":
# Wait 1 second before calling `ai_move()` to make it look like the computer is "thinking".
game.after(1000, ai_move)
def make_button(space_index):
"""
Creates a button in the given space
"""
button = tk.Button(button_holder, text=" ", font="Times 20 bold",
bg="gray", fg="white", height=4, width=8)
row = space_index // 3 # use the "floor division" operator to get the row
col = space_index % 3 # use the "modulo" operator to get the column
# Put the button in the window using TK's "grid" method
button.grid(row=row, column=col, stick=tk.S + tk.N + tk.E + tk.W)
# Call on_click() when the button is clicked
button.bind("<Button-1>", on_click)
# return the button object for use later on
return button
def inverse_board():
return [space * -1 for space in board]
def ai_move():
if AI_MODE == "random":
random_ai_move()
elif AI_MODE == "ml":
ml_ai_move()
def random_ai_move():
valid_space = False
while not valid_space:
space_index = random.randint(0, 8)
valid_space = board[space_index] == 0
make_move(space_index)
def ml_ai_move():
model = ml4k.Model(API_KEY)
ai_board = inverse_board()
print("Finding best move for board:", ai_board)
result = model.classify(ai_board)
print(result)
label = result['class_name']
space_index = labels.index(label)
# If the chosen space is occupied, choose a random one instead.
if board[space_index] == 0:
make_move(space_index)
else:
print("space is occupied, choosing random")
random_ai_move()
def train_winning_moves():
API_KEY = "7b00fe60-46ad-11e9-aaea-ff8de7d1c9f769c3f786-6375-4ec1-9dad-f2d808999ba9"
model = ml4k.Model(API_KEY)
print("---")
for move in player_moves:
space = move[0]
board_state = move[1]
label = labels[space]
print("Adding training data for move: " + label)
model.add_training_data(label, board_state)
# The rest of the code is called immediately when the script starts:
new_game()
# Start the app
game.mainloop()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment