# File: Othello.py # Athor: D. Vrajitoru # C463 Artificial Intelligence # Fall 2025 # An implementation of an agent capable of playing the game of Othello # against a human, and the setup to be able to play the game. import tkinter as tk from random import * from time import * size = 8 directions = ['N', 'S', 'E', 'W', 'NE', 'NW', 'SE', 'SW'] empty = 0 bwidth = 600 bheight = 600 padr = 0 padc = 0 cell_size = 10 class Othello: # Initial state of the board def __init__ (self, who = 1): self.board = [] self.turn = who for i in range(size): self.board.append([]) for j in range(size): self.board[i].append(empty) self.board[3][3] = 1 self.board[3][4] = -1 self.board[4][3] = -1 self.board[4][4] = 1 self.game_over = False # Starts a new game def new_game(self): for i in range(size): for j in range(size): self.board[i][j] = empty self.board[3][3] = 1 self.board[3][4] = -1 self.board[4][3] = -1 self.board[4][4] = 1 self.print_board() self.draw_game() self.label.configure(text="Click to make a move.") self.game_over = False # Makes a copy of the current board and returns it. def copy_board(b): newb = [] for i in range(size): newb.append([]) for j in range(size): newb[i].append(b[i][j]) return newb # Gives the directions in which the two coordinate increment for each # of the 8 directions of movement. Incomplete - to be completed by # the student. def dir_move(dir): if dir == 'N': return -1, 0 elif dir == 'E': return 0, 1 elif dir == 'SW': return 1, -1 else: return 1, 1 # Find out if by moving to position (i, j) we capture any of the # opponents pieces in that direction of movement. This function # returns the number of pieces of the opponent that can be captured # this way, so that it can be used for evaluating the move in case you # want to go only one level deep. def find_opp_dir (b, i, j, who, dir): # di and dj are the increments for the row and column based on # the direction of movement di, dj = Othello.dir_move(dir) i1 = i + di j1 = j + dj tokens = 0 # i1 and j1 are scanning indexes in the direction of # movement if not (i1 < size and j1 >= 0 and j1 < size): return 0 # if the cell is not empty, return false if not (b[i][j] == 0 and b[i1][j1] == who * -1): return 0 # while we're still on the board # and we haven't found one of our pieces while i1 >= 0 and i1 < size and j1 >= 0 and j1 < size: if b[i1][j1] == who * -1: # as long as we're on the opposite pieces # move in the directions di, dj i1 = i1 + di j1 = j1 + dj tokens += 1 elif b[i1][j1] == who: # if we find our piece after a string of opponent pieces return tokens else: # if we run into a space return false return 0 # If we ran out of the board we return false return 0 # Reverse all the opponents pieces in a given direction of # movement. We can assume for this one that we already checked that # we can capture something in that direction of movement. def reverse_line_dir (b, i, j, who, dir): di, dj = Othello.dir_move(dir) i1 = i + di j1 = j + dj while i1 >= 0 and i1 < size and j1 >= 0 and j1 < size and \ b[i1][j1] != who and b[i1][j1] != empty: b[i1][j1] = who i1 += di j1 += dj return True # Compute the score for one particular player as the number of pieces # on the board belonging to them minus the number of pieces belonging # to the opponent. def score_board(b, who): result = 0 for i in range(size): for j in range(size): if b[i][j] == who: result += 1 elif b[i][j] == who * -1: result -= 1 return result # score the game's own board def score(self, who): return Othello.score_board(self.board, who) # A function checking if the position (i, j) on the board b is a possible # legal move for the player who. def possible_move(b, i, j, who): if b[i][j] == 0: for dir in directions: # If we can capture anything in the direction of movement, # then this is a legal move for this player. if Othello.find_opp_dir(b, i, j, who, dir): return True return False # Checks if the player "who" can play its token on the position i j # and makes the move if possible. def move_board(b, i, j, who): # if the cell is not free, return nil. result = False if b[i][j] == 0: for dir in directions: # If we can capture anything in the direction of movement, # then we reverse all of the opponents pieces in that # direction and set the result to true. Since we never set # it back to false, if we can reverse anything from this # position, then the function will return true, otherwise # false. if Othello.find_opp_dir(b, i, j, who, dir): result = True Othello.reverse_line_dir(b, i, j, who, dir) # If the previous loop caused the result to be true, we set the # cell we're looking at to one of our pieces. if result : b[i][j] = who return result # Function move for the player itself def move(self, i, j, who): return Othello.move_board(self.board, i, j, who) # AI player of the game. This function should return the two values i, j # defining the position on the board of the next move of the player "who" # based on the current situation on the board b. The function must return a # legal move or -1, -1 if no move is possible for that player. def next_move(b, who): # To be written by the student return -1, -1 def print_board(self): Othello.print_a_board(self.board) # Print the board such that it's fairly easy to read. # Works for a console version of the game def print_a_board(state): print(" 0 1 2 3 4 5 6 7") print(" +---+---+---+---+---+---+---+---+") c = 0 for i in range(size): print(i, "|", end=" ") for j in range(size): c = state[i][j] if c == 0: print(" ", end=" ") elif c == 1: print("*", end=" ") else: print("O", end=" ") print("|", end=" ") print(" ") print(" +---+---+---+---+---+---+---+---+") # Initial state of the board #(print_a_board board) ## 0 1 2 3 4 5 6 7 ## +---+---+---+---+---+---+---+---+ ## 0 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ ## 1 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ ## 2 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ ## 3 | | | | * | O | | | | ## +---+---+---+---+---+---+---+---+ ## 4 | | | | O | * | | | | ## +---+---+---+---+---+---+---+---+ ## 5 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ ## 6 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ ## 7 | | | | | | | | | ## +---+---+---+---+---+---+---+---+ # A function that makes the move chosen by the playerfind the best next move and does it. It also prints # out what move it made. I added the parameter who so that we can play # two programs against each other, so one will call this function with # the parameter being 1, the other being -1. def play_me(self, i, j): return self.move(i, j, self.turn) # A function that find the best next move and does it. It also prints # out what move it made. I added the parameter who so that we can play # two programs against each other, so one will call this function with # the parameter being 1, the other being -1. def play_other(self): who = -1 * self.turn # Opponent's id # Get the next move by calling the function i, j = Othello.next_move(self.board, who) if i >-1 and j>-1: print("NPC next move is:", i, j) # just a debugging statement # Apply the move to the current board self.move(i, j, who) else: print("I have no move, pass") self.print_board() print("My score is:", self.score(self.turn)) #convert screen coordinates to row and column def row(self, y): return (y-padr) // cell_size def col(self, x): return (x-padc) // cell_size # Returns true if neither player can make any other move in the game def no_more_moves(self): # to be completed by the student return False # Checks if the game is over and returns true if it is, sets the variable # game_over, and displays a message saying if the player has won or lost. def check_end_game(self): count0 = count_me = count_opp = 0 for i in range(size): for j in range(size): if self.board[i][j] == empty: count0 += 1 elif self.board[i][j] == self.turn: count_me += 1 else: count_opp += 1 if count0 == 0 or self.no_more_moves(): if count_me == count_opp: self.label.configure(text="Game Over. It's a draw.") self.game_over = True return True elif count_me > count_opp: self.label.configure(text="Game Over. You won.") self.game_over = True return True else: self.label.configure(text="Game Over. You lost.") self.game_over = True return True elif count_me == 0: self.label.configure(text="Game Over. You lost.") self.game_over = True return True elif count_opp == 0: self.label.configure(text="Game Over. You won.") self.game_over = True return True else: return False # Callback function for mouse click events. Makes a move for the black player # at the position of the mouse click. After a short delay, it makes a move of the # white player using the AI function. def on_click(self, event): if self.game_over: return r = self.row(event.y) c = self.col(event.x) if not self.play_me(r, c): self.label.configure(text="Movement not allowed; pass") else: self.draw_game() # redraw the board after a move self.check_end_game() if self.game_over: return sleep(1) # a small delay before making the opponent's move self.play_other() # let the opponent make a move self.draw_game() # then redraw the game # Converts a number of columns into an x position on the canvas def x(self, col): return padc + cell_size * col # Converts a number of rows into a y position on the canvas def y(self, row): return padr + cell_size * row # Draws the grid on the canvas: a big dark blue block for the overall, # and small light blue squares for the spaces. def draw_game(self): self.canvas.delete("all") # the big square covering everything self.canvas.create_rectangle(0, 0, bwidth+5, bheight+5, fill="gray", \ outline="darkgrey", width=1) for r in range(size): for c in range(size): # add squares for the spaces and the target self.canvas.create_rectangle(self.x(c), self.y(r), self.x(c+1), self.y(r+1), \ fill="green", outline="darkgreen", width=2) if self.board[r][c] == 1: self.canvas.create_oval(self.x(c)+2, self.y(r)+2, self.x(c+1)-2, self.y(r+1)-2, \ fill="black", outline="white", width=2) elif self.board[r][c] == -1: self.canvas.create_oval(self.x(c)+2, self.y(r)+2, self.x(c+1)-2, self.y(r+1)-2, \ fill="white", outline="black", width=2) self.app.update() # refresh the window # Creates the window in tkinter and adds all the elements to it def open_window(self): global padr, padc, cell_size cell_size = min([(bheight-10) // size, (bwidth-10) // size]) padr = (bheight - cell_size * size) // 2 + 4 padc = (bwidth - cell_size * size) // 2 + 4 print(padr, padc) # Create the window self.app = tk.Tk() self.app.title("Othello") # Create a frame to contain the buttons frame = tk.Frame(self.app, borderwidth=2, relief="groove") frame.pack() # Add the buttons with left alignment so they all show in a row button1 = tk.Button(frame, text="New Game", command=self.new_game) button1.pack(side='left') # Add a canvas for displaying the grid self.canvas = tk.Canvas(self.app, width = bwidth, height = bheight, bg="gray", bd=2, relief="sunken") self.canvas.pack(pady=5, padx=5) self.draw_game() # Add key movements # for k in ('w', 'W', 'a', 'A', 's', 'S', 'd', 'D'): # self.app.bind(k, self.key_press) # Add a label where we can display some text for the user self.label = tk.Label(self.app, text="Click to place token") self.label.pack() # Add mouse click # Bind the left-click event to the app window self.app.bind("", self.on_click) if __name__ == '__main__': # This is only a test to see that the program works: game = Othello(1) game.print_board() # Example of how you make a move # print(game.move(5, 3, 1)) # print(game.score(1)) # game.print_board() game.open_window() game.app.mainloop()