3
\$\begingroup\$

I wrote an Othello version with limited UI to try to learn python(3.7) and OOP.
The game has 8 classes.
Start creates a controller object which is responsible for the logical flow of the game. Controller creates AI objects that chooses moves based on difficulty settings.
Start also creates a GameFrame object which controls the UI (GameFrame is passed two function from Controller so that Controller can respond to player actions). GameFrame has the Tk(), and adds ScoreFrame(score and status), GameButtons(main game frame) and playerselectUI(frame). the GameButtons class populates the board with GameButton which at the moment functions like a regular tkinter.Button

I would really like some feedback regarding my OOP choices. I've never done peer review before and I'm curious if my structure makes sense to anyone else, and if my code is readable.

Thank you for your time.

Start.py

from Controller import Controller
from GameFrame import GameFrame

# Set up a controller object, and give it access to the graphical representation of the board
# Board is passed the click and startnewgame functions from the controller so that controller
# can decide what happens when buttons are clicked

if __name__ == '__main__':
    controller = Controller()
    game = GameFrame("title" ,controller.click, controller.start_new_game)
    controller.game = game
    game.start()
pass

Controller.py

from Ai import Ai
class Controller(object):

# Set up a 8x8 board of zeroes (empty spots) ones(black bricks) and twos (white bricks)
# AIs set to None (Human controllers) at start
def __init__(self):
    self.empty = 0
    self.player = 1
    self.opponent = 2
    self.ais = [None, None]       
    self.buttons = [[0 for _ in range(8)]for _ in range(8)]
    self.reset_board()

def reset_board(self):
    for x in range(8):
        for y in range(8):
            self.buttons[x][y] = 0
    self.buttons[3][3] = 2
    self.buttons[4][4] = 2
    self.buttons[3][4] = 1
    self.buttons[4][3] = 1
    self.whiteCount = 2
    self.blackCount = 2
    self.emptyCount = 60
    self.player = 1
    self.opponent = 2

# Reset the board to starting positions. 
#Set AIs as selected by the player in playerSelectUI. Start the first move    
# p1 and p2 are ints
def start_new_game(self, p1, p2):
    self.reset_board()
    self.game.reset_board()
    self.ais= [self.get_AI_level_or_human(p1), self.get_AI_level_or_human(p2)]
    self.next_move()

# 0 returns easiest AI, 1 normal AI, 2 hard AI.
# 3 returns None which sets player as human
# x is an int
def get_AI_level_or_human(self, x):
    if x == 3:
        return None
    else:
        return Ai(x)

# Check if player has a move to perform, if not switch player.
# If that player doesn't have any legal moves either -> end game
# If one of the players has a move to perform,
# do the AIs move, or wait for player to click a move
def next_move(self):
    if len(self.find_all_legal_moves()) == 0:
        self.update_status(self.get_player_color() + " has no legal moves, switching player")
        self.switch_player()
        if len(self.find_all_legal_moves()) == 0:
            self.update_status("No more legal moves, ending game. " + self.get_winner())
            return None

    if self.emptyCount == 0:
        self.update_status("No more empty squares, ending game. " + self.get_winner())
    else:
        if self.ais[self.player-1] != None:
            self.game.root.after(100,self.ai_move)

# Get all legal moves and have the AI determine which move to make
# Flip those bricks
def ai_move(self):
    toFlip = self.ais[self.player-1].make_move(self.find_all_legal_moves())
    self.perform_move(toFlip)

# Check every square on the board to see if a brick can be placed there.
# Return a list of squares which are legal
# and which bricks will be turned if you place brick there
def find_all_legal_moves(self):
    legal = []
    lega = []
    for x in range(8):
        for y in range(8):
            lega = self.check_if_legal(x, y)
            if len(lega) > 0:
                legal.append(lega)

    return legal

# Called from the GameButtons when a player clicks. 
# Check if player can place brick at that square, and perform the turn
# x and y are ints
def click(self, x, y):
    toTurn = self.check_if_legal(x,y)
    if len(toTurn) > 0:
        self.perform_move(toTurn)
    else: self.update_status(
        "That's not a legal move, try again. Current player: " + self.get_player_color())

# check if current player is white or black
def get_player_color(self):
    if self.player == 2:
        return "white"
    else: return "black"

# If current player is black, change to white. Else change to black.
def switch_player(self):
    self.opponent = self.player
    if self.player == 1:
        self.player = 2
    else: self.player = 1
    self.update_status("Current player: " + self.get_player_color())

# Update scoreboard. If the move placed 5 white bricks on the board
# then black must have lost 4 and "empty" lost one.
# x is int and represents how many bricks were altered
def update_count(self, x):
    if self.player == 2:
        self.whiteCount += x
        self.blackCount -= x-1
    else:
        self.whiteCount -= x-1
        self.blackCount += x
    self.emptyCount -= 1
    self.game.update_texts(self.whiteCount, self.blackCount, self.emptyCount)

# After move has been selected update gameboard and graphics to reflect changes.
# Swap current player and opponent
# Prepare for next move.
# toTurn is a list of squares to turn
def perform_move(self, toTurn):
    self.flip_buttons(toTurn)
    self.update_count(len(toTurn))
    self.switch_player()
    self.next_move()

# Look at one particular square and see if current player can place a brick there.
# If yes, return a list of squares that now will get players color
def check_if_legal(self,x,y):
    toTurn = []
    if self.buttons[x][y] == self.empty:
        for i in range(9):
            if i == 4:
                continue
            toTurnDir = self.check_direction(x,y,self.get_Y_direction(i), self.get_X_direction(i))
            if toTurnDir != None:
                toTurn += toTurnDir

        if len(toTurn) > 0:
                toTurn.append([x, y])

    return toTurn  

# Change values in the gameboard and change the graphics for those squares
def flip_buttons(self, toTurn):
    for brick in toTurn:
        self.buttons[brick[0]][brick[1]] = self.player
        self.game.change_color(brick[0], brick[1],self.get_player_color())

# Get direction to move along the Y axis(rows), -1:up, 0:no movement, 1:up     
def get_Y_direction(self,y):
    dirX = y % 3 - 1
    return dirX

# Get direction to move along X axis (column), -1:left, 0:no movement, 1:right
def get_X_direction(self, x):
    dirY = x // 3 - 1
    return dirY

# Check that square being clicked is next to an opponent.
# Keep checking that direction as long as there's an opponent
# If you run outside the board or hit an empty square:
# return None indicating no bricks will be turned in that direction
# If you find a brick of your own color after finding an opponent then direction is legal.
# Return list of squares that will be turned
def check_direction(self, x, y, dirX, dirY):
    toTurn = []
    if x+dirX > 7 or x + dirX < 0 or y + dirY < 0 or y + dirY > 7:
        return None
    if self.buttons[x+dirX][y+dirY] == self.opponent:
        toTurn.append([x+dirX, y+dirY])
        for i in range(2,8):
            if x + dirX * i > 7 or x + dirX*i < 0 or y + dirY*i < 0 or y + dirY*i > 7:
                return None
            if self.buttons[x+dirX*i][y+dirY*i] == self.opponent:
                toTurn.append([x+dirX*i, y+dirY*i])
            if self.buttons[x+dirX*i][y+dirY*i] == self.empty:
                return None
            if self.buttons[x+dirX*i][y+dirY*i] == self.player:
                return toTurn

def update_status(self, text):
    self.game.scoreFrame.update_status(text)

def get_winner(self):
    if self.whiteCount > self.blackCount:
        return "White wins the game"
    if self.blackCount > self.whiteCount:
        return "Black wins the game"
    return "The game ends in a tie"

Ai.py

import random as ran
class Ai(object):

def __init__(self, difficulty):
    '''
    Constructor
    '''
    self.difficulty = difficulty 
# Select a move from the list of legal moves, based on what difficulty AI is in use.    
def make_move(self, possibleMoves):
    if self.difficulty == 0:
        return self.random_move(possibleMoves)
    if self.difficulty == 1:
        return self.greedy(possibleMoves)
    if self.difficulty == 2:
        return self.hard(possibleMoves)

# Select a random move out of the legal ones.
# Known as "easy" difficulty
def random_move(self, possibleMoves):
    select =  ran.randrange(len(possibleMoves))
    return possibleMoves[select]

# Check the list of legal moves and select the one that will turn the most bricks.
# If several moves turns the same amount of bricks, return a random one of them
# Known as "normal" difficulty
def greedy(self, possibleMoves):
    biggest = 0
    greediestMoves = []
    for x in range(len(possibleMoves)):
        if len(possibleMoves[x]) == biggest:
            greediestMoves.append(possibleMoves[x])
        if len(possibleMoves[x]) > biggest:
            biggest = len(possibleMoves[x])
            greediestMoves = []
            greediestMoves.append(possibleMoves[x])

    select =  ran.randrange(len(greediestMoves))
    return greediestMoves[select]

# Values indicating how "good" a square is to have, used only by the hardest AI
square_values = [ 
        [99, -8, 8,  6,  6,  8,  -8, 99], 
        [-8, -24,  -4, -3, -3, -4, -24,  -8],  
        [8, -4, 7,  4,  4,  7,  -4, 8], 
        [6,  -3, 4,  0,  0,  4,  -3, 6],  
        [6,  -3, 4,  0,  0,  4,  -3, 6],
        [8,  -4, 7,  4,  4,  7,  -4, 8],
        [-8, -24,  -4, -3, -3, -4, -24, -8],
        [99, -8, 8,  6,  6,  8,  -8, 99] 
      ]

# Go trough the list of legal moves to find the move that adds the most "value" based on the valuegrid.
# If several moves add the same "value", randomly return one of them 
# Known as "hard" difficulty
def hard(self, possibleMoves):
    best = -99
    bestMoves = []
    for x in range(len(possibleMoves)):

        value = self.square_values[possibleMoves[x][-1][0]][possibleMoves[x][-1][1]]
        if value == best:
            bestMoves.append(possibleMoves[x])
            best = value
        if value > best:
            bestMoves = [possibleMoves[x]]
            best = value
    return bestMoves[ran.randrange(len(bestMoves))]

GameFrame.py

from GameButtons import GameButtons
from tkinter import Tk, Frame, BOTH, X
from ScoreFrame import ScoreFrame
from playerSelectUI import PlayerSelectUi

class GameFrame(Frame):
# Main UI frame. Starts a Tk() and adds other frames to it.

def __init__(self, params, click, start_new_game):
    self.root = Tk()
    self.root.geometry("500x600")
    self.scoreFrame = ScoreFrame(self.root)
    self.scoreFrame.update_texts(2, 2, 60)
    self.scoreFrame.pack(fill = X)   
    self.buttons = GameButtons(click)
    self.buttons.pack(fill=BOTH, expand=1)
    self.playerSelect = PlayerSelectUi(start_new_game)
    self.playerSelect.pack(fill = BOTH)

def change_color(self, x, y, color):
    self.buttons.change_color(x, y, color)

def update_texts(self, white, black, empty):
    self.scoreFrame.update_texts(white, black, empty)

def start(self):
    self.root.mainloop()

def reset_board(self):
    self.buttons.reset_board()

ScoreFrame.py

from tkinter import Frame, StringVar, E,W, Label
class ScoreFrame(Frame):
'''
Set up a frame where scores are displayed. (Current amount of black/white and empty bricks)
Class used by and added to GameFrame
'''
def __init__(self, params):
    '''
    Constructor
    Set up StringVars and add them to the labels
    '''

    super().__init__()
    self.whiteTextVar = StringVar()
    self.blackTextVar = StringVar()
    self.emptyTextVar = StringVar()
    self.columnconfigure(0, weight = 1)
    self.columnconfigure(1, weight = 1)
    self.columnconfigure(2, weight = 1)
    self.whiteTextVar.set("White discs: ")
    self.blackTextVar.set("Black discs: ")
    self.emptyTextVar.set("Empty discs: ")
    self.whiteCount = Label(self, textvariable = self.whiteTextVar, bg = "white")
    self.whiteCount.grid(row = 0, column = 0, sticky=(E, W))
    self.blackCount = Label(self, textvariable = self.blackTextVar, bg = "red")
    self.blackCount.grid(row = 0, column = 1, sticky=(E, W))
    self.emptyCount = Label(self, textvariable = self.emptyTextVar, bg = "yellow")
    self.emptyCount.grid(row = 0, column = 2, sticky=(E, W))
    self.status_text = StringVar()
    self.status_text.set("")
    self.status_bar = Label(self, textvariable = self.status_text)
    self.status_bar.grid(row = 1, column = 0, columnspan = 3, sticky=(E,W))

# Change the textVariables to update the text.
def update_texts(self, white, black, empty):
    self.whiteTextVar.set("White discs: %s" %white)
    self.blackTextVar.set("Black discs: %s" %black)
    self.emptyTextVar.set("Empty discs: %s" %empty)

def update_status(self, text):
    self.status_text.set(text)

GameButtons.py

from tkinter import E, W, N, S, Frame, PhotoImage
from GameButton import GameButton
class GameButtons(Frame):

# Create a frame with the buttons for the game, is used by GameFrame
def __init__(self, click):
    super().__init__()
    self.buttons = [[0 for _ in range(8)]for _ in range(8)]
    for x in range(8):
        for y in range(8):
            b = GameButton(self, 0)
            b.grid(row = x, column = y,sticky=(N, S, E, W))
            b.configure(text = "%s%s"%(x,y), command = lambda row = x, col = y: click(row, col))
            self.buttons[x][y] = b
            self.columnconfigure(y, weight = 1)
        self.rowconfigure(x, weight = 1)
    self.reset_board()

def change_color(self, x, y, color):
    self.buttons[x][y].configure(bg = color)

def get_brick(self, x, y):
    return self.buttons[x][y].get_brick()

def reset_board(self):
    for x in range(8):
        for y in range(8):
            self.buttons[x][y].configure(bg = "green")

    self.buttons[3][3].configure(bg = "white")
    self.buttons[4][4].configure(bg = "white")
    self.buttons[4][3].configure(bg = "black")
    self.buttons[3][4].configure(bg = "black")

playerSelectUI.py

from tkinter import Frame, Label, Button, SUNKEN, RAISED, N, S, E, W
class PlayerSelectUi(Frame):
'''
Set up a frame where the user can select AI / Human controller for each of the players
Class used by and added to GameFrame
'''
# Set up the UI that allows player to select difficulty of opponent
# start_new_game is a function passed from Controller
def __init__(self, start_new_game):
    '''
    Constructor
    '''
    super().__init__()
    self.selection1 = [None] * 4
    self.selection2 = [None] * 4
    self.selectedP1 = 3 #default to human controller
    self.selectedP2 = 3

    self.p1label = Label(self, text = "Player 1")
    self.p1label.grid(row = 0, column = 0)
    self.selection1[0] = self.player1SelectButton1 = Button(self, text = "Easy")
    self.player1SelectButton1.grid(row = 0, column = 1,sticky=(N, S, E, W))
    self.player1SelectButton1.configure(command = lambda: self.clickP1(0))
    self.selection1[1] = self.player1SelectButton2 = Button(self, text = "Normal")
    self.player1SelectButton2.grid(row = 0, column = 2, sticky = (N, S, E, W))
    self.player1SelectButton2.configure(command = lambda: self.clickP1(1))
    self.selection1[2] = self.player1SelectButton3 = Button(self, text = "Hard")
    self.player1SelectButton3.grid(row = 0, column = 3,sticky=(N, S, E, W))
    self.player1SelectButton3.configure(command = lambda: self.clickP1(2))
    self.selection1[3] = self.player1SelectButton4 = Button(self, text = "Human")
    self.player1SelectButton4.grid(row = 0, column = 4,sticky=(N, S, E, W))
    self.player1SelectButton4.configure(
        relief = SUNKEN, command = lambda: self.clickP1(3))

    self.p2label = Label(self, text = "Player 2")
    self.p2label.grid(row = 1, column = 0)
    self.selection2[0] = self.player2SelectButton1 = Button(self, text = "Easy")
    self.player2SelectButton1.grid(row = 1, column = 1,sticky=(N, S, E, W))
    self.player2SelectButton1.configure(command = lambda: self.clickP2(0))
    self.selection2[1] = self.player2SelectButton2 = Button(self, text = "Normal")
    self.player2SelectButton2.grid(row = 1, column = 2, sticky = (N, S, E, W))
    self.player2SelectButton2.configure(command = lambda: self.clickP2(1))
    self.selection2[2] = self.player2SelectButton3 = Button(self, text = "Hard")
    self.player2SelectButton3.grid(row = 1, column = 3,sticky=(N, S, E, W))
    self.player2SelectButton3.configure(command = lambda: self.clickP2(2))
    self.selection2[3] = self.player2SelectButton4 = Button(self, text = "Human")
    self.player2SelectButton4.grid(row = 1, column = 4,sticky=(N, S, E, W))
    self.player2SelectButton4.configure(
        relief = SUNKEN, command = lambda: self.clickP2(3))

    self.startGameButton = Button(self, text = "Start Game")
    self.startGameButton.grid(row = 2, column = 0)
    self.startGameButton.configure(
        command = lambda: start_new_game(self.selectedP1, self.selectedP2))

# Select difficulty of player1. Can set as AI or human player. 
# x 0 = easy, 1 = normal, 2 = hard, 3 = human
def clickP1(self, x):
    self.selectedP1 = x
    for b in range(len(self.selection1)):
        if b == x:
            self.selection1[b].configure(relief =SUNKEN)
        else:
            self.selection1[b].configure(relief=RAISED)

# Select difficulty of player2. Can set as AI or human player
def clickP2(self, x):
    self.selectedP2 = x
    for b in range(len(self.selection2)):
        if b == x:
            self.selection2[b].configure(relief =SUNKEN)
        else:
            self.selection2[b].configure(relief=RAISED)

GameButton.py

from tkinter import Button
class GameButton(Button):
'''
classdocs
extends tkinter.Button to be able to store a value.
Class currently adds no value and could be replaced with tkinter.Button.
'''

def __init__(self, master, brick):
    Button.__init__(self, master)
    self.brick = brick

# Currently not used            
def get_brick(self):
    return self.brick

#def animate_flip():
#def change_icon():
\$\endgroup\$
  • \$\begingroup\$ The indentation for some of your code is formatted incorrectly. \$\endgroup\$ – Bryan Oakley Aug 28 '18 at 3:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.