I'm trying to build a simple 'Connect 4' game. In the game, I'd like a menu with radiobuttons to present itself to the user at every turn, so that he/she could pick the next move.
I've tried putting the menu into a "while" loop, but it simply doesn't do any action. I don't know how to fix it, because I don't understand exactly how tkinter works. I've seen some other questions touching on this subject, but I still can't figure out how to fix it.
Below is the code, along with the two classes I used along with it. The menu I'm trying to present in a loop is in 'present_columns_choice', which is being used in a loop in 'handle_two_humans'
Would be grateful for any help. Thanks!
from game import *
from tkinter import *
HUMAN = 0
COMPUTER = 1
GRID_SIZE = 50
NUM_ROWS = 6
NUM_COLUMNS = 7
COLOR_A = "red"
COLOR_B = "blue"
class GameGUI:
def __init__(self, root):
self.__root = root
self.__player_A = None
self.__player_B = None
def assign_player(self, player, identity):
if player == "player A":
self.__player_A = identity
elif player == "player B":
self.__player_B = identity
print("Player A is: " + str(self.__player_A))
print("Player B is: " + str(self.__player_B))
def present_player_choice(self):
self.ask_choose_player("player A")
self.ask_choose_player("player B")
Button(self.__root, text="OK", command=quit).pack(anchor=W)
def ask_choose_player(self, player):
Label(self.__root, text="Who would you like to play " + player + "?").pack(anchor=W)
var = IntVar()
Radiobutton(self.__root, text="human", variable=var,
command=lambda:self.assign_player(player, HUMAN), value=1).pack(anchor=W)
Radiobutton(self.__root, text="computer", variable=var,
command=lambda:self.assign_player(player, COMPUTER), value=2).pack(anchor=W)
def get_playerA(self):
return self.__player_A
def get_playerB(self):
return self.__player_B
def handle_two_humans(self):
game = Game()
canvas_width = GRID_SIZE*NUM_COLUMNS
canvas_height = GRID_SIZE*NUM_ROWS
canvas = Canvas(self.__root, width=canvas_width, height=canvas_height)
canvas.pack()
for row_ind in range(NUM_ROWS):
for column_ind in range(NUM_COLUMNS):
canvas.create_rectangle(column_ind*GRID_SIZE, row_ind*GRID_SIZE,
(column_ind+1)*GRID_SIZE, (row_ind+1)*GRID_SIZE)
while not IS_GAME_WON:
self.present_columns_choice(game, canvas)
def add_disc(self, game, column, canvas):
current_player = game.get_current_player()
if current_player == PLAYER_A:
self.fill_square(game, canvas, COLOR_A, column)
elif current_player == PLAYER_B:
self.fill_square(game, canvas, COLOR_B, column)
game.make_move(column)
def present_columns_choice(self, game, canvas):
columns = game.get_board().get_available_columns()
var = IntVar()
Label(self.__root, text="The following columns are still available. "
"Where would you like to place your disc?").pack(anchor=W)
for ind, column in enumerate(columns):
shown_column = column+1
Radiobutton(self.__root, text=shown_column, padx=20, variable=var, value=ind,
command=lambda column=column: self.add_disc(game, column, canvas)).pack(anchor=W)
Button(self.__root, text="OK", command=quit).pack(anchor=W)
def fill_square(self, game, canvas, color, column):
""" Fills square of column chosen by player. """
row = game.get_board().get_available_row(column)
canvas.create_rectangle(column*GRID_SIZE, row*GRID_SIZE, (column+1)*GRID_SIZE,
(row+1)*GRID_SIZE, fill = color)
if __name__ == '__main__':
root = Tk()
gui = GameGUI(root)
gui.handle_two_humans()
mainloop()
Here's the Game class, which was used here:
from board import *
PLAYER_A = 1
PLAYER_B = 2
INITIAL_ROW = 0
INITIAL_COLUMN = 0
FINAL_ROW = 5
FINAL_COLUMN = 6
ILLEGAL_LOCATION_MSG = "Illegal location."
ILLEGAL_MOVE_MSG = "Illegal move."
IS_GAME_WON = False
WINNER = None
class Game:
def __init__(self):
self.__current_player = PLAYER_A
self.__board = Board()
self.__is_game_won = False
self.__winner = None
def make_move(self, column):
""" Makes move and updates board and current player, if column is a valid choice and game is ongoing. """
possible_winner = self.__current_player
if self.__board.is_col_illegal(column) or self.__is_game_won:
raise Exception(ILLEGAL_MOVE_MSG)
self.do_move(column)
if self.__board.is_win(column):
self.__is_game_won = True
self.__winner = possible_winner
def do_move(self, column):
""" Actual implementation of the move. """
if self.__current_player == PLAYER_A:
self.__board.update_board(column, PLAYER_A)
self.__current_player = PLAYER_B
elif self.__current_player == PLAYER_B:
self.__board.update_board(column, PLAYER_B)
self.__current_player = PLAYER_A
def get_winner(self):
""" Returns winner, or None if there is none. """
return self.__winner
def get_player_at(self, row, col):
""" Returns the player whose disc is at the given position in the game. """
if row < INITIAL_ROW or row > FINAL_ROW or col < INITIAL_COLUMN or col > FINAL_COLUMN:
raise Exception(ILLEGAL_LOCATION_MSG)
return self.__board.get_board()[row][col]
def get_current_player(self):
""" Returns current_player. """
return self.__current_player
def get_board(self):
""" Returns board."""
return self.__board
And here's Board, which is used in Game:
NUM_ROWS = 6
NUM_COLUMNS = 7
INITIAL_VALUE = None
WIN_COUNT = 4
FIRST_ROW = 5
LAST_ROW = 0
class Board:
def __init__(self):
self.__playboard = []
self.__available_rows_list = NUM_COLUMNS*[NUM_ROWS-1]
initial_row = NUM_COLUMNS * [INITIAL_VALUE]
for i in range(NUM_ROWS):
self.__playboard.append(initial_row.copy())
def get_available_columns(self):
""" Returns all columns that still have empty space in them. """
available_columns = []
for col in range(len(self.__available_rows_list)):
if self.__available_rows_list[col] >= 0:
available_columns.append(col)
return available_columns
def get_playboard(self):
""" Returns board. """
return self.__playboard
def update_board(self, col, val):
""" Updates current status of board. """
row = self.__available_rows_list[col]
self.__playboard[row][col] = val
self.__update_row(col)
def __update_row(self, col):
""" Updates available_row_list. """
self.__available_rows_list[col] = self.__available_rows_list[col] - 1
def __is_col_available(self, col):
""" Checks if given col has empty spaces left on the playboard. """
if self.__available_rows_list[col] < 0:
return False
return True
def __is_col_exist(self, col):
""" Checks if given column is within the capacity of the playboard. """
if col < 0 or col >= NUM_COLUMNS:
return False
return True
def is_col_illegal(self, col):
""" Checks if given column is an illegal option. """
if not self.__is_col_available(col) or not self.__is_col_exist(col):
return True
return False
def print_playboard(self):
for row in self.__playboard:
print(row)
def is_win(self, col):
""" Checks if current state of board resulted in a win. """
row = self.__available_rows_list[col]+1
if self.__check_vertical_win(row, col) or self.__check_horizontal_win(row, col) or \
self.__check_decreasing_diagonal_win(row, col) or self.__check_increasing_diagonal_win(row, col):
return True
return False
def __check_increasing_diagonal_win(self, original_row, original_col):
""" Checks if player has won in the increasing diagonal direction. """
count = 1
player = self.__playboard[original_row][original_col]
col = original_col + 1
row = original_row - 1
while self.__is_col_exist(col) and row >= LAST_ROW and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col + 1
row = row - 1
# Then: undergo same process, this time in the opposite direction.
col = original_col - 1
row = original_row + 1
while self.__is_col_exist(col) and row <= FIRST_ROW and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col - 1
row = row + 1
def __check_decreasing_diagonal_win(self, original_row, original_col):
""" Checks if player has won in the decreasing diagonal direction. """
count = 1
player = self.__playboard[original_row][original_col]
col = original_col + 1
row = original_row + 1
while self.__is_col_exist(col) and row <= FIRST_ROW and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col + 1
row = row + 1
# Then: undergo same process, this time in the opposite direction.
col = original_col - 1
row = original_row - 1
while self.__is_col_exist(col) and row >= LAST_ROW and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col - 1
row = row - 1
def __check_vertical_win(self, original_row, col):
""" Checks if player has won in the horizontal direction. """
count = 1
player = self.__playboard[original_row][col]
row = original_row + 1
while row <= FIRST_ROW and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
row = row + 1
def __check_horizontal_win(self, row, original_col):
""" Checks if player has won in the horizontal direction. """
count = 1
player = self.__playboard[row][original_col]
col = original_col + 1
while self.__is_col_exist(col) and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col + 1
# Then: undergo same process, this time in the opposite direction (the left).
col = original_col - 1
while self.__is_col_exist(col) and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col - 1
def get_available_row(self, column):
""" Returns the row which will be filled if you choose this column. """
return self.__available_rows_list[column]
The while loop inside handle_two_humans() will block the tkinter mainloop.
Since you do the move if a radiobutton is selected in the menu, why don't you just do the move when you click on the board?
Create a function check_move() in GameGUI class:
def check_move(self, game, column, canvas):
if not game.get_winner():
self.add_disc(game, column, canvas)
And modify handle_two_humands() as below:
def handle_two_humans(self):
game = Game()
canvas_width = GRID_SIZE*NUM_COLUMNS
canvas_height = GRID_SIZE*NUM_ROWS
canvas = Canvas(self.__root, width=canvas_width, height=canvas_height)
canvas.pack()
for row_ind in range(NUM_ROWS):
for column_ind in range(NUM_COLUMNS):
canvas.create_rectangle(column_ind*GRID_SIZE, row_ind*GRID_SIZE,
(column_ind+1)*GRID_SIZE, (row_ind+1)*GRID_SIZE)
# bind mouse click event to perform move
# e.x//GRID_SIZE will get the clicked column
canvas.bind('<Button-1>', lambda e: self.check_move(game, e.x//GRID_SIZE, canvas))
Related
I recently tried to code an AI to solve the connect-four game. I've come quite far but I am now stuck with a minor mistake in the code that I just cant locate. Generally the algorithm played great but sometimes the algorithms ignores a row of 3 pieces the opponent has which results in the loss of the algorithm. As you will see, I have constructed the evaluation function so that positions like that should be rated with a extremely low score, which it does. Also the score of a position where the algorithm has lost is always rated with -inf. Therefore I can't imagine why the algorithm would be unable to counter positions like that.
The bot relies on a framework which I can't upload here thus I am sorry that the code itself without changes cant be simply executed.
from aiagent import AiAgent
import math
import copy
import numpy as np
class MinMaxAgent(AiAgent):
def __init__(self):
'''
Creates the board.
'''
self.board = [[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0],
[0,0,0,0,0,0]]
def getNextMove(self):
'''
Calculate the index of the player move and store it on the board. Return that value.
'''
self._getMinMax(board=self.board)
self.board[self.bestIndexCol][self._getRowIndex(self.bestIndexCol, self.board)] = 1
print(f'Eval: {self._evaluate(self.board)}')
return (self.bestIndexCol, self._getRowIndex(self.bestIndexCol, self.board))
def handleNextMove(self, indexCol):
'''
Store the index of the enemy move in the board.
'''
self.board[indexCol[0]][self._getRowIndex(indexCol[0], self.board)] = -1
print(f'Eval: {self._evaluate(self.board)}')
def _getRowIndex(self, indexCol, board):
'''
Get the index of the row of a column within a board.
'''
for indexRow, elementRow in enumerate(board[indexCol]):
if elementRow == 0:
return indexRow
def _getValidIndex(self, board):
'''
Get all the valid indices of a board.
'''
validMoves = []
for indexCol, col in enumerate(board):
if col.count(0) != 0:
validMoves.append(indexCol)
return validMoves
def _getMinMax(self, board, depth=6, player=1, alpha=-math.inf, beta=math.inf):
'''
Calculates the best move within a specific depth.
'''
if depth == 0:
return self._evaluate(board)
elif self._isTerminalState(board) == 0:
return self._evaluate(board)
elif self._isTerminalState(board) == -1:
return -math.inf
elif self._isTerminalState(board) == 1:
return math.inf
if player == 1:
resultMax = -math.inf
self.bestIndexCol = None
for indexCol in self._getValidIndex(board):
# Mutate the board
self.nextBoard = copy.deepcopy(board)
self.nextBoard[indexCol][self._getRowIndex(indexCol, board)] = 1
# Calls itself with a by one decremented depth and the change of player
self.resultMinMax = self._getMinMax(board=self.nextBoard, depth=depth-1, player=-1, alpha=alpha, beta=beta)
# Take the board state with the most amount of points
if self.resultMinMax > resultMax:
resultMax = self.resultMinMax
self.bestIndexCol = indexCol
# Change alpha if the boardstate is evaluated with more points
if self.resultMinMax > alpha:
alpha = self.resultMinMax
# Break the loop if on a alphaboundry
if alpha >= beta:
break
return resultMax
elif player == -1:
resultMin = math.inf
for indexCol in self._getValidIndex(board):
# Mutate the board
self.nextBoard = copy.deepcopy(board)
self.nextBoard[indexCol][self._getRowIndex(indexCol, board)] = -1
# Calls itself with a by one decremented depth and the change of player
self.resultMinMax = self._getMinMax(board=self.nextBoard, depth=depth-1, player=1, alpha=alpha, beta=beta)
# Take the board state with the least amount of points
if self.resultMinMax < resultMin:
resultMin = self.resultMinMax
# Change beta if the boardstate is evaluated with less points
if self.resultMinMax < beta:
beta = self.resultMinMax
# Break the loop if on a betaboundry
if alpha >= beta:
break
return resultMin
def _isTerminalState(self, board):
'''
Checks the board for a terminal state of the board:
Return: 0 for a draw;
1 for a victory;
-1 for a defeat;
'''
# Evaluate draw
if [board[col].count(0) for col in range(7)] == [0,0,0,0,0,0,0]:
return 0
# Evaluate vertical for terminal state
for col in range(7): # columns
for row in range(3): # rows
if [board[col][row + i] for i in range(4)] == [1,1,1,1]:
return 1
elif [board[col][row + i] for i in range(4)] == [-1,-1,-1,-1]:
return -1
# Evaluate horizontal for terminal state
for col in range(4): # columns
for row in range(6): # rows
if [board[col + i][row] for i in range(4)] == [1,1,1,1]:
return 1
elif [board[col + i][row] for i in range(4)] == [-1,-1,-1,-1]:
return -1
# Evaluate diagonal for terminal state
for col in range(4): # columns
for row in range(3): # rows
if [board[col + i][row + i] for i in range(4)] == [1,1,1,1]:
return 1
elif [board[col + i][row + i] for i in range(4)] == [-1,-1,-1,-1]:
return -1
for col in range(4): # columns
for row in range(3, 6): # rows
if [board[col + i][row - i] for i in range(4)] == [1,1,1,1]:
return 1
elif [board[col + i][row - i] for i in range(4)] == [-1,-1,-1,-1]:
return -1
def _evaluateSection(self, section):
'''
Evaluates every section of the board and adds points according to the amount of elements of the same actor in a section:
PLAYER: 4-in-a-row: +inf
ENEMY: 4-in-a-row: -inf
PLAYER: 3-in-a-row: +1000
ENEMY: 3-in-a-row: -3000
PLAYER: 2-in-a-row: +200
ENEMY: 2-in-a-row: -600
'''
self.section_evaluation = 0
if section.count(1) == 4:
self.section_evaluation += math.inf
elif section.count(-1) == 4:
self.section_evaluation -= math.inf
elif section.count(1) == 3 and section.count(0) == 1:
self.section_evaluation += 1000
elif section.count(-1) == 3 and section.count(0) == 1:
self.section_evaluation -= 3000
elif section.count(1) == 2 and section.count(0) == 2:
self.section_evaluation += 200
elif section.count(-1) == 2 and section.count(0) == 2:
self.section_evaluation -= 600
return self.section_evaluation
def _evaluate(self, board):
'''
Takes sections of the board to evaluate.
'''
self.evaluation = 0
# Evaluate vertical sections
for col in range(7): # columns
for row in range(3): # rows
self.section = [board[col][row + i] for i in range(4)]
self.evaluation += self._evaluateSection(self.section)
# Evaluate horizontal sections
for col in range(4): # columns
for row in range(6): # rows
self.section = [board[col + i][row] for i in range(4)]
self.evaluation += self._evaluateSection(self.section)
# Evaluate diagonal sections
for col in range(4): # columns
for row in range(3): # rows
self.section = [board[col + i][row + i] for i in range(4)]
self.evaluation += self._evaluateSection(self.section)
for col in range(4): # columns
for row in range(3, 6): # rows
self.section = [board[col + i][row - i] for i in range(4)]
self.evaluation += self._evaluateSection(self.section)
return self.evaluation
I already redesigned the evaluation function and checked the minmax-algorithm which should include all the possible sources of this error but I wasn't able to find any satisfying answer.
Currently am testing a connect four with minimax implementation and was playing around with numpy arrays and a regular matrix. I tried running the minimax function with a board created with a regular list matrix as opposed to a numpy array and it kept filling up the board state instead of dropping just one piece. I thought creating a shallow copy of the board state and passing it through wouldn't affect things, but I'm a bit stumped on this. It works fine with an numpy array.
import numpy as np
import random
import math
import copy
ROW_COUNT = 6
COLUMN_COUNT = 7
PLAYER = 0
AI = 1
EMPTY = 0
PLAYER_PIECE = 1
AI_PIECE = 2
WINDOW_LENGTH = 4
def create_board():
board = [[0 for x in range(COLUMN_COUNT)] for i in range(ROW_COUNT)]
# board = np.zeros((ROW_COUNT,COLUMN_COUNT), dtype=int)
return board
def drop_piece(board, row, col, piece):
board[row][col] = piece
def is_valid_location(board, col):
return board[0][col] == 0
def get_next_open_row(board, col):
for r in range(ROW_COUNT-1, -1, -1):
if board[r][col] == 0:
return r
def print_board(board):
# print(np.flip(board, 0))
for i in board:
print(i)
def winning_move(board, piece):
# Check horizontal locations for win
for c in range(COLUMN_COUNT-3):
for r in range(ROW_COUNT):
if board[r][c] == piece and board[r][c+1] == piece and board[r][c+2] == piece and board[r][c+3] == piece:
return True
# Check vertical locations for win
for c in range(COLUMN_COUNT):
for r in range(ROW_COUNT-3):
if board[r][c] == piece and board[r+1][c] == piece and board[r+2][c] == piece and board[r+3][c] == piece:
return True
# Check positively sloped diaganols
for c in range(COLUMN_COUNT-3):
for r in range(ROW_COUNT-3):
if board[r][c] == piece and board[r+1][c+1] == piece and board[r+2][c+2] == piece and board[r+3][c+3] == piece:
return True
# Check negatively sloped diaganols
for c in range(COLUMN_COUNT-3):
for r in range(3, ROW_COUNT):
if board[r][c] == piece and board[r-1][c+1] == piece and board[r-2][c+2] == piece and board[r-3][c+3] == piece:
return True
def evaluate_window(window, piece):
score = 0
opp_piece = PLAYER_PIECE
if piece == PLAYER_PIECE:
opp_piece = AI_PIECE
if window.count(piece) == 4:
score += 100
elif window.count(piece) == 3 and window.count(EMPTY) == 1:
score += 5
elif window.count(piece) == 2 and window.count(EMPTY) == 2:
score += 2
if window.count(opp_piece) == 3 and window.count(EMPTY) == 1:
score -= 4
return score
def score_position(board, piece):
score = 0
## Score center column
# center_array = [int(i) for i in list(board[:, COLUMN_COUNT//2])]
center_array = [int(i) for i in [col[COLUMN_COUNT//2] for col in board]]
center_count = center_array.count(piece)
score += center_count * 3
## Score Horizontal
for r in range(ROW_COUNT):
# row_array = [int(i) for i in list(board[r,:])]
row_array = [i for i in board[r]]
for c in range(COLUMN_COUNT-3):
window = row_array[c:c+WINDOW_LENGTH]
score += evaluate_window(window, piece)
## Score Vertical
for c in range(COLUMN_COUNT):
# col_array = [int(i) for i in list(board[:,c])]
col_array = [col[c] for col in board]
for r in range(ROW_COUNT-3):
window = col_array[r:r+WINDOW_LENGTH]
score += evaluate_window(window, piece)
## Score posiive sloped diagonal
for r in range(ROW_COUNT-3):
for c in range(COLUMN_COUNT-3):
window = [board[r+i][c+i] for i in range(WINDOW_LENGTH)]
score += evaluate_window(window, piece)
for r in range(ROW_COUNT-3):
for c in range(COLUMN_COUNT-3):
window = [board[r+3-i][c+i] for i in range(WINDOW_LENGTH)]
score += evaluate_window(window, piece)
return score
def is_terminal_node(board):
return winning_move(board, PLAYER_PIECE) or winning_move(board, AI_PIECE) or len(get_valid_locations(board)) == 0
def minimax(board, depth, alpha, beta, maximizingPlayer):
valid_locations = get_valid_locations(board)
is_terminal = is_terminal_node(board)
if depth == 0 or is_terminal:
if is_terminal:
if winning_move(board, AI_PIECE):
return (None, 100000000000000)
elif winning_move(board, PLAYER_PIECE):
return (None, -10000000000000)
else: # Game is over, no more valid moves
return (None, 0)
else: # Depth is zero
return (None, score_position(board, AI_PIECE))
if maximizingPlayer:
value = -math.inf
column = random.choice(valid_locations)
for col in valid_locations:
row = get_next_open_row(board, col)
b_copy = board.copy()
drop_piece(b_copy, row, col, AI_PIECE)
new_score = minimax(b_copy, depth-1, alpha, beta, False)[1]
if new_score > value:
value = new_score
column = col
alpha = max(alpha, value)
if alpha >= beta:
break
return column, value
else: # Minimizing player
value = math.inf
column = random.choice(valid_locations)
for col in valid_locations:
row = get_next_open_row(board, col)
b_copy = board.copy()
drop_piece(b_copy, row, col, PLAYER_PIECE)
new_score = minimax(b_copy, depth-1, alpha, beta, True)[1]
if new_score < value:
value = new_score
column = col
beta = min(beta, value)
if alpha >= beta:
break
return column, value
def get_valid_locations(board):
valid_locations = []
for col in range(COLUMN_COUNT):
if is_valid_location(board, col):
valid_locations.append(col)
return valid_locations
def pick_best_move(board, piece):
valid_locations = get_valid_locations(board)
best_score = -10000
best_col = random.choice(valid_locations)
for col in valid_locations:
row = get_next_open_row(board, col)
temp_board = board.copy()
drop_piece(temp_board, row, col, piece)
score = score_position(temp_board, piece)
if score > best_score:
best_score = score
best_col = col
return best_col
Testing here, dropping just one piece and then running minimax to get the best piece.
board = create_board()
print_board(board)
print("\n")
game_over = False
if is_valid_location(board,1):
row = get_next_open_row(board,1)
drop_piece(board, row, 1, PLAYER_PIECE)
print_board(board)
print("\n")
checker, score = minimax(board, 5, -math.inf, math.inf, True)
if is_valid_location(board,checker):
row = get_next_open_row(board,checker)
drop_piece(board, row, checker, AI_PIECE)
print_board(board)
print("\n")
I wrote a little code for a class called Game, which uses another class called Board.
One of the functions in Board is check_horizontal_win(self, row, original_col).
When I use it directly on a Board object it works fine. When I try to use it with the Game's board (Game.get_board().check_horizontal_win)) I get:
AttributeError: 'function' object has no attribute 'check_horizontal_win'
I've checked and I don't seem to be using anything private or illegal. And the values in the two cases were the same.
Here's the first class, Game. The last line of code in the "main" doesn't work:
from board import *
PLAYER_A = 1
PLAYER_B = 2
INITIAL_ROW = 0
INITIAL_COLUMN = 0
FINAL_ROW = 5
FINAL_COLUMN = 6
ILLEGAL_LOCATION_MSG = "Illegal location."
ILLEGAL_MOVE_MSG = "Illegal move."
IS_GAME_WON = False
WINNER = None
class Game:
def __init__(self):
self.__current_player = PLAYER_A
self.__board = Board()
def make_move(self, column):
""" Makes move and updates board and current player, if column is a valid choice and game is ongoing. """
if self.__board.is_col_illegal(column):
raise Exception(ILLEGAL_MOVE_MSG)
self.do_move(column)
def do_move(self, column):
""" Actual implementation of the move. """
if self.__current_player == PLAYER_A:
self.__board.update_board(column, PLAYER_A)
self.__current_player = PLAYER_B
elif self.__current_player == PLAYER_B:
self.__board.update_board(column, PLAYER_B)
self.__current_player = PLAYER_A
def get_winner(self):
pass
def get_player_at(self, row, col):
""" Returns the player whose disc is at the given position in the game. """
if row < INITIAL_ROW or row > FINAL_ROW or col < INITIAL_COLUMN or col > FINAL_COLUMN:
raise Exception(ILLEGAL_LOCATION_MSG)
return self.__board.get_board()[row][col]
def get_current_player(self):
""" Returns current_player. """
return self.__current_player
def get_board(self):
""" Returns board."""
return self.__board
if __name__ == '__main__':
game = Game()
game.make_move(0)
game.make_move(1)
game.make_move(0)
game.make_move(2)
game.get_board.check_horizontal_win(0, 1)
And here's the Board class. In this case, the function in the main does work:
NUM_ROWS = 6
NUM_COLUMNS = 7
INITIAL_VALUE = None
WIN_COUNT = 4
class Board:
def __init__(self):
self.__playboard = []
self.__available_rows_list = NUM_COLUMNS*[NUM_ROWS-1]
initial_row = NUM_COLUMNS * [INITIAL_VALUE]
for i in range(NUM_ROWS):
self.__playboard.append(initial_row.copy())
def get_playboard(self):
""" Returns board. """
return self.__playboard
def update_board(self, col, val):
""" Updates current status of board. """
row = self.__available_rows_list[col]
self.__playboard[row][col] = val
self.__update_row(col)
def __update_row(self, col):
""" Updates available_row_list. """
if self.__available_rows_list[col] > 0:
self.__available_rows_list[col] = self.__available_rows_list[col] - 1
else:
self.__available_rows_list[col] = None
def __is_col_available(self, col):
""" Checks if given col has empty spaces left on the playboard. """
if self.__available_rows_list[col] == None:
return False
return True
def __is_col_exist(self, col):
""" Checks if given column is within the capacity of the playboard. """
if col < 0 or col >= NUM_COLUMNS:
return False
return True
def is_col_illegal(self, col):
""" Checks if given column is an illegal option. """
if not self.__is_col_available(col) or not self.__is_col_exist(col):
return True
return False
def print_playboard(self):
for row in self.__playboard:
print(row)
def check_horizontal_win(self, row, original_col):
""" Checks if player has won in the horizontal direction. """
# check if row and col were valid? Or do it somewhere else?
count = 1
player = self.__playboard[row][original_col]
col = original_col
while self.__is_col_exist(col) and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col + 1
# Then: undergo same process, this time in the opposite direction (the left).
col = original_col - 1
while self.__is_col_exist(col) and self.__playboard[row][col] == player:
count = count + 1
if count == WIN_COUNT:
return True
col = col - 1
if __name__ == '__main__':
board = Board()
board.update_board(0,1)
board.update_board(0, 1)
board.update_board(1, 2)
board.update_board(2, 2)
board.print_playboard()
board.check_horizontal_win(0, 1)
You wrote:
game.get_board.check_horizontal_win(0, 1)
Make that:
game.get_board().check_horizontal_win(0, 1)
I am trying to write a program for connect 4 but am having a lot of trouble getting past the directions. Everything under the comment, "#everything works up to here" works but then it all explodes and I have no idea even where to start to fix it.
#connect 4
import random
#define global variables
X = "X"
O = "O"
EMPTY = "_"
TIE = "TIE"
NUM_ROWS = 6
NUM_COLS = 8
def display_instruct():
"""Display game instructions."""
print(
"""
Welcome to the second greatest intellectual challenge of all time: Connect4.
This will be a showdown between your human brain and my silicon processor.
You will make your move known by entering a column number, 1 - 7. Your move
(if that column isn't already filled) will move to the lowest available position.
Prepare yourself, human. May the Schwartz be with you! \n
"""
)
def ask_yes_no(question):
"""Ask a yes or no question."""
response = None
while response not in ("y", "n"):
response = input(question).lower()
return response
def ask_number(question,low,high):
"""Ask for a number within range."""
#using range in Python sense-i.e., to ask for
#a number between 1 and 7, call ask_number with low=1, high=8
low=1
high=NUM_COLS
response = None
while response not in range (low,high):
response=int(input(question))
return response
def pieces():
"""Determine if player or computer goes first."""
go_first = ask_yes_no("Do you require the first move? (y/n): ")
if go_first == "y":
print("\nThen take the first move. You will need it.")
human = X
computer = O
else:
print("\nYour bravery will be your undoing... I will go first.")
computer = X
human = O
return computer, human
def new_board():
board = []
for x in range (NUM_COLS):
board.append([" "]*NUM_ROWS)
return board
def display_board(board):
"""Display game board on screen."""
for r in range(NUM_ROWS):
print_row(board,r)
print("\n")
def print_row(board, num):
"""Print specified row from current board"""
this_row = board[num]
print("\n\t| ", this_row[num], "|", this_row[num], "|", this_row[num], "|", this_row[num], "|", this_row[num], "|", this_row[num], "|", this_row[num],"|")
print("\t", "|---|---|---|---|---|---|---|")
# everything works up to here!
def legal_moves(board):
"""Create list of column numbers where a player can drop piece"""
legal=True
while not legal:
col = input("What column would you like to move into (1-7)?")
for row in range (6,0,1):
if (1 <= row <= 6) and (1 <= col <= 7) and (board[row][col]==" "):
board[row][col] = turn
legal = True
else:
print("Sorry, that is not a legal move.")
def human_move(board,human):
"""Get human move"""
try:
legals = legal_moves(board)
move = None
while move not in legals:
move = ask_number("Which column will you move to? (1-7):", 1, NUM_COLS)
if move not in legals:
print("\nThat column is already full, nerdling. Choose another.\n")
print("Human moving to column", move)
return move #return the column number chosen by user
except NameError:
print ("Only numbers are allowed.")
except IndexError:
print ("You can only select colums from 1-7.")
def get_move_row(turn,move):
for m in (NUM_COLS):
place_piece(turn,move)
display_board()
def computer_move ():
move= random.choice(legal)
return move
def place_piece(turn,move):
if this_row[m[move]]==" ":
this_row.append[m[move]]=turn
def winner(board):
# Check rows for winner
for row in range(6):
for col in range(3):
if (board[row][col] == board[row][col + 1] == board[row][col + 2] == board[row][col + 3]) and (board[row][col] != " "):
return [row][col]
# Check columns for winner
for col in range(6):
for row in range(3):
if (board[row][col] == board[row + 1][col] == board[row + 2][col] ==board[row + 3][col]) and (board[row][col] != " "):
return [row][col]
# Check diagonal (top-left to bottom-right) for winner
for row in range(3):
for col in range (4):
if (board[row][col] == board[row + 1][col + 1] == board[row + 2][col + 2] == board[row + 3][col + 3]) and (board[row][col] != " "):
return true
# Check diagonal (bottom-left to top-right) for winner
for row in range (5,2,-1):
for col in range (3):
if (board[row][col] == board[row - 1][col + 1] == board[row - 2][col + 2] == board[row - 3][col + 3]) and (board[row][col] != " "):
return [row][col]
# No winner
return False
def main():
display_instruct()
computer,human = pieces()
turn = X
board = new_board()
while not winner(board) and (" " not in board):
display_board(board)
if turn == human:
human_move(board,human)
get_move_row()
place_piece()
else:
computer_move(board,computer)
place_piece()
display_board(board)
turn = next_turn()
the_winner = winner(board)
congrat_winner(the_winner, computer, human)
#start the program
main ()
input ("\nPress the enter key to quit.")
For fun, here's an object-oriented refactorization. It's a bit long, but well documented and should be easy to understand.
I started with your code and split it into Board, Player, and Game classes, then derived Computer and Human classes from Player.
Board knows the shape and size of the rack, what moves are legal, and recognizes when wins and ties occur
Player has a name and knows how to choose (or prompt for) a legal move
Game has a Board and two Players and controls turn-taking and output
I'm not 100% happy with it - Board has a .board that is a list of list of string, but Game has a .board that is a Board; a bit of judicious renaming would be a good idea - but for an hour's work it's pretty solid.
Hope you find this educational:
# Connect-4
from itertools import cycle, groupby
from random import choice
from textwrap import dedent
import sys
# version compatibility shims
if sys.hexversion < 0x3000000:
# Python 2.x
inp = raw_input
rng = xrange
else:
# Python 3.x
inp = input
rng = range
def get_yn(prompt, default=None, truthy={"y", "yes"}, falsy={"n", "no"}):
"""
Prompt for yes-or-no input
Return default if answer is blank and default is set
Return True if answer is in truthy
Return False if answer is in falsy
"""
while True:
yn = inp(prompt).strip().lower()
if not yn and default is not None:
return default
elif yn in truthy:
return True
elif yn in falsy:
return False
def get_int(prompt, lo=None, hi=None):
"""
Prompt for integer input
If lo is set, result must be >= lo
If hi is set, result must be <= hi
"""
while True:
try:
value = int(inp(prompt))
if (lo is None or lo <= value) and (hi is None or value <= hi):
return value
except ValueError:
pass
def four_in_a_row(tokens):
"""
If there are four identical tokens in a row, return True
"""
for val,iterable in groupby(tokens):
if sum(1 for i in iterable) >= 4:
return True
return False
class Board:
class BoardWon (BaseException): pass
class BoardTied(BaseException): pass
EMPTY = " . "
HOR = "---"
P1 = " X "
P2 = " O "
VER = "|"
def __init__(self, width=8, height=6):
self.width = width
self.height = height
self.board = [[Board.EMPTY] * width for h in rng(height)]
self.tokens = cycle([Board.P1, Board.P2])
self.rowfmt = Board.VER + Board.VER.join("{}" for col in rng(width)) + Board.VER
self.rule = Board.VER + Board.VER.join(Board.HOR for col in rng(width)) + Board.VER
def __str__(self):
lines = []
for row in self.board:
lines.append(self.rowfmt.format(*row))
lines.append(self.rule)
lines.append(self.rowfmt.format(*("{:^3d}".format(i) for i in rng(1, self.width+1))))
lines.append("")
return "\n".join(lines)
def is_board_full(self):
return not any(cell == Board.EMPTY for cell in self.board[0])
def is_win_through(self, row, col):
"""
Check for any winning sequences which pass through self.board[row][col]
(This is called every time a move is made;
thus any win must involve the last move,
and it is faster to check just a few cells
instead of the entire board each time)
"""
# check vertical
down = min(3, row)
up = min(3, self.height - row - 1)
tokens = [self.board[r][col] for r in rng(row - down, row + up + 1)]
if four_in_a_row(tokens):
return True
# check horizontal
left = min(3, col)
right = min(3, self.width - col - 1)
tokens = [self.board[row][c] for c in rng(col - left, col + right + 1)]
if four_in_a_row(tokens):
return True
# check upward diagonal
down = left = min(3, row, col)
up = right = min(3, self.height - row - 1, self.width - col - 1)
tokens = [self.board[r][c] for r,c in zip(rng(row - down, row + up + 1), rng(col - left, col + right + 1))]
if four_in_a_row(tokens):
return True
# check downward diagonal
down = right = min(3, row, self.width - col - 1)
up = left = min(3, self.height - row - 1, col)
tokens = [self.board[r][c] for r,c in zip(rng(row - down, row + up + 1), rng(col + right, col - left - 1, -1))]
if four_in_a_row(tokens):
return True
# none of the above
return False
def legal_moves(self):
"""
Return a list of columns which are not full
"""
return [col for col,val in enumerate(self.board[0], 1) if val == Board.EMPTY]
def do_move(self, column):
token = next(self.tokens)
col = column - 1
# column is full?
if self.board[0][col] != Board.EMPTY:
next(self.move) # reset player token
raise ValueError
# find lowest empty cell (guaranteed to find one)
for row in rng(self.height-1, -1, -1): # go from bottom to top
if self.board[row][col] == Board.EMPTY: # find first empty cell
# take cell
self.board[row][col] = token
# did that result in a win?
if self.is_win_through(row, col):
raise Board.BoardWon
# if not, did it result in a full board?
if self.is_board_full():
raise Board.BoardTied
# done
break
class Player:
def __init__(self, name):
self.name = name
def get_move(self, board):
"""
Given the current board state, return the row to which you want to add a token
"""
# you should derive from this class instead of using it directly
raise NotImplemented
class Computer(Player):
def get_move(self, board):
return choice(board.legal_moves())
class Human(Player):
def get_move(self, board):
legal_moves = board.legal_moves()
while True:
move = get_int("Which column? (1-{}) ".format(board.width), lo=1, hi=board.width)
if move in legal_moves:
return move
else:
print("Please pick a column that is not already full!")
class Game:
welcome = dedent("""
Welcome to the second greatest intellectual challenge of all time: Connect4.
This will be a showdown between your human brain and my silicon processor.
You will make your move known by entering a column number, 1 - 7. Your move
(if that column isn't already filled) will move to the lowest available position.
Prepare yourself, human. May the Schwartz be with you!
""")
def __init__(self):
print(Game.welcome)
# set up new board
self.board = Board()
# set up players
self.players = cycle([Human("Dave"), Computer("HAL")])
# who moves first?
if get_yn("Do you want the first move? (Y/n) ", True):
print("You will need it...\n")
# default order is correct
else:
print("Your rashness will be your downfall...\n")
next(self.players)
def play(self):
for player in self.players:
print(self.board)
while True:
col = player.get_move(self.board) # get desired column
try:
print("{} picked Column {}".format(player.name, col))
self.board.do_move(col) # make the move
break
except ValueError:
print("Bad column choice - you can't move there")
# try again
except Board.BoardWon:
print("{} won the game!".format(player.name))
return
except Board.BoardTied:
print("The game ended in a stalemate")
return
def main():
while True:
Game().play()
if not get_yn("Do you want to play again? (Y/n) ", True):
break
if __name__=="__main__":
main()
I am trying to program a version of conway's game of life however I keep getting the message 'Cell' Object Has no attribute 'nextState' when it seems like it should declare the value of nextState before asking to reference it. here is my code:
from tkinter import *
root = Tk()
class Cell (Button):
Dead = 0
Live = 1
def __init__ (self,parent):
Button.__init__(self,parent, relief = "raised" , width = 2 , borderwidth = 1 , command = self.onpress)
self.displayState(Cell.Dead)
def onpress (self):
if self.state == Cell.Live:
self.displayState(Cell.Dead)
elif self.state == Cell.Dead:
self.displayState(Cell.Live)
def setNextState (self , Neighbours):
if self.state == Cell.Live and (Neighbours < 2 or Neighbours > 3):
self.nextState = Cell.Dead
elif self.state == Cell.Dead and Neighbours == 3:
self.nextState = Cell.Live
elif self.state == Cell.Dead and Neighbours != 3:
self.nextState = self.state
def stepToNextState(self):
self.displayState(self.nextState)
def displayState (self , newstate):
self.state = newstate
if self.state == Cell.Live:
self["bg"] = "black"
if self.state == Cell.Dead:
self["bg"] = "white"
class Grid:
def __init__(self,parent,sizex,sizey):
self.sizex = sizex
self.sizey = sizey
self.cells = []
for a in range (0,self.sizex):
rowcells = []
for b in range (0, self.sizey):
c = Cell(parent)
c.grid(row=b , column=a)
rowcells.append(c)
self.cells.append(rowcells)
def step (self):
cells = self.cells
for x in range (0,self.sizex):
if x==0: x_down = self.sizex-1
else: x_down = x-1
if x==self.sizex-1: x_up = 0
else: x_up = x+1
for y in range(0,self.sizey):
if y==0: y_down = self.sizey-1
else: Y_down = y-1
if y==self.sizey-1: y_up = 0
else: y_up = y+1
sum = cells[x_down][y].state + cells[x_up][y].state + cells[x][y_down].state + cells[x][y_up].state + cells[x_down][y_down].state +cells[x_up][y_up].state + cells[x_down][y_up].state + cells[x_up][y_down].state
cells[x][y].setNextState(sum)
for row in cells:
for cell in row:
cell.stepToNextState()
def clear(self):
for row in self.cells:
for cell in row:
cell.displayState(Cell.Dead)
if __name__ == "__main__":
frame = Frame(root)
frame.pack()
grid = Grid(frame,25,25)
bottomFrame = Frame(root)
bottomFrame.pack (side = BOTTOM)
buttonStep = Button(bottomFrame , text="Step" , command=grid.step)
buttonStep.pack(side = LEFT)
buttonClear = Button(bottomFrame, text = "Clear", command=grid.clear)
buttonClear.pack(side=LEFT , after=buttonStep)
root.mainloop()
The error message is:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python33\lib\tkinter\__init__.py", line 1442, in __call__
return self.func(*args)
File "C:\Users\Owner\Desktop\Other\Programs & Misc\Python\Tyler's Game of Life.py", line 65, in step
cell.stepToNextState()
File "C:\Users\Owner\Desktop\Other\Programs & Misc\Python\Tyler's Game of Life.py", line 27, in stepToNextState
self.displayState(self.nextState)
AttributeError: 'Cell' object has no attribute 'nextState'
If anyone could point out where the error is occuring / what is causing it and possibly a way to fix it I would be very grateful.
The issue appears to be that your for row in cells loop is inside of your previous for x in range(0, self.sizex) loop. Here's what the step method should look like if you get it correctly indented:
def step (self):
cells = self.cells
for x in range (0,self.sizex):
if x==0: x_down = self.sizex-1
else: x_down = x-1
if x==self.sizex-1: x_up = 0
else: x_up = x+1
for y in range(0,self.sizey):
if y==0: y_down = self.sizey-1
else: Y_down = y-1
if y==self.sizey-1: y_up = 0
else: y_up = y+1
sum = cells[x_down][y].state + cells[x_up][y].state + cells[x][y_down].state + cells[x][y_up].state + cells[x_down][y_down].state +cells[x_up][y_up].state + cells[x_down][y_up].state + cells[x_up][y_down].state
cells[x][y].setNextState(sum)
for row in cells: # unindent these
for cell in row: # lines by one
cell.stepToNextState() # level each
If all the indentation issues are taken care of (or were not in the original code), there's still an issue that may cause an issue in certain situations. The issue is that the Cell.setNextState method doesn't handle every situation. Specifically, it doesn't set nextState if a cell is alive and should stay so (it has two or three living neighbors). The lack of an else on your chain of if and elif statements should have raised this as a red flag for me, but I overlooked it the first time I examined the function.
Here's how it can be fixed:
def setNextState (self , Neighbours):
if self.state == Cell.Live and (Neighbours < 2 or Neighbours > 3):
self.nextState = Cell.Dead
elif self.state == Cell.Dead and Neighbours == 3:
self.nextState = Cell.Live
else: # remove the conditions on this block, all the state changes were handled above
self.nextState = self.state