Files

329 lines
12 KiB
Python

import argparse
import math
from copy import deepcopy
class Player:
def __init__(self, player_number, board_symbol):
self._player_number = player_number
self._board_symbol = board_symbol
@property
def board_symbol(self):
return self._board_symbol
@property
def player_number(self):
return self._player_number
def get_move(self, board):
raise NotImplementedError
class HumanPlayer(Player):
def __init__(self, player_number, board_symbol):
super(HumanPlayer, self).__init__(player_number, board_symbol)
def get_move(self, _):
valid_input = False
column = None
row = None
while not valid_input:
try:
column = int(input("Enter Column: "))
row = int(input("Enter Row: "))
valid_input = True
except ValueError:
print("Value entered was not an integer, please input column and row again...")
return column, row
class MinimaxPlayer(Player):
def __init__(self, player_number, board_symbol):
super(MinimaxPlayer, self).__init__(player_number, board_symbol)
def utility(self, board, player_symbol):
if player_symbol == "X":
return board.count_score("O") - board.count_score("X")
else:
return board.count_score("X") - board.count_score("O")
def successors(self, board, player_symbol):
successors = []
for row in range(board.num_rows):
for column in range(board.num_columns):
if board.cell_empty(column, row):
if board.is_legal_move(column, row, player_symbol):
successors.append((column, row))
return successors
def minimax_max_value(self, board, current_player_symbol):
if not board.has_legal_moves_remaining(current_player_symbol):
return self.utility(board, current_player_symbol), (None, None)
best_found_value = -math.inf, (None, None)
for successor in self.successors(board, current_player_symbol):
new_board = deepcopy(board)
new_board.play_move(*successor, current_player_symbol)
next_player_symbol = "O" if current_player_symbol == "X" else "X"
new_board_minimax = self.minimax_min_value(new_board, next_player_symbol)
if new_board_minimax[0] > best_found_value[0]:
best_found_value = new_board_minimax[0], successor
return best_found_value
def minimax_min_value(self, board, current_player_symbol):
if not board.has_legal_moves_remaining(current_player_symbol):
return self.utility(board, current_player_symbol), (None, None)
best_found_value = math.inf, (None, None)
for successor in self.successors(board, current_player_symbol):
new_board = deepcopy(board)
new_board.play_move(*successor, current_player_symbol)
next_player_symbol = "O" if current_player_symbol == "X" else "X"
new_board_minimax = self.minimax_max_value(new_board, next_player_symbol)
if new_board_minimax[0] < best_found_value[0]:
best_found_value = new_board_minimax[0], successor
return best_found_value
def minimax_decision(self, board, current_player_symbol):
minimax_result = self.minimax_max_value(board, current_player_symbol)
return minimax_result[1]
def get_move(self, board):
return self.minimax_decision(board, self.board_symbol)
class OthelloBoard:
EMPTY = "."
DIRECTIONS = {
"N": {"column": 0, "row": 1},
"NE": {"column": 1, "row": 1},
"E": {"column": 1, "row": 0},
"SE": {"column": 1, "row": -1},
"S": {"column": 0, "row": -1},
"SW": {"column": -1, "row": -1},
"W": {"column": -1, "row": 0},
"NW": {"column": -1, "row": 1},
}
def __init__(self, num_columns, num_rows, player_1_board_symbol, player_2_board_symbol):
self._columns = num_columns
self._rows = num_rows
self.grid = [[self.EMPTY for _ in range(self.num_rows)] for _ in range(self.num_columns)]
self.set_cell(self.num_columns // 2, self.num_rows // 2, player_1_board_symbol)
self.set_cell(self.num_columns // 2 - 1, self.num_rows // 2 - 1, player_1_board_symbol)
self.set_cell(self.num_columns // 2 - 1, self.num_rows // 2, player_2_board_symbol)
self.set_cell(self.num_columns // 2, self.num_rows // 2 - 1, player_2_board_symbol)
self.display()
@property
def num_columns(self):
return self._columns
@property
def num_rows(self):
return self._rows
def in_bounds(self, column, row):
return (0 <= column < self.num_columns) and (0 <= row < self.num_rows)
def get_cell(self, column, row):
assert self.in_bounds(column, row)
return self.grid[row][column]
def set_cell(self, column, row, value):
assert self.in_bounds(column, row)
self.grid[row][column] = value
def cell_empty(self, column, row):
assert self.in_bounds(column, row)
return self.grid[row][column] == self.EMPTY
def display(self):
for row_index in reversed(range(self.num_rows)):
print("{}:| ".format(row_index), end="")
for column_index in range(self.num_rows):
print("{} ".format(self.get_cell(column_index, row_index)), end="")
print()
print(" -", end="")
for _ in range(self.num_columns):
print("--", end="")
print()
print(" ", end="")
for column_index in range(self.num_columns):
print("{} ".format(column_index), end="")
print("\n")
def set_coordinates_in_direction(self, column, row, direction):
direction_offsets = self.DIRECTIONS[direction]
return column + direction_offsets["column"], row + direction_offsets["row"]
def check_endpoint(self, column, row, player_symbol, direction, match_symbol):
if not self.in_bounds(column, row) or self.cell_empty(column, row):
return False
if match_symbol:
if self.get_cell(column, row) == player_symbol:
return True
else:
next_column, next_row = self.set_coordinates_in_direction(column, row, direction)
return self.check_endpoint(next_column, next_row, player_symbol, direction, match_symbol)
else:
if self.get_cell(column, row) == player_symbol:
return False
else:
next_column, next_row = self.set_coordinates_in_direction(column, row, direction)
return self.check_endpoint(next_column, next_row, player_symbol, direction, not match_symbol)
def flip_pieces_helper(self, column, row, player_symbol, direction):
if self.get_cell(column, row) == player_symbol:
return 0
self.set_cell(column, row, player_symbol)
next_column, next_row = self.set_coordinates_in_direction(column, row, direction)
return 1 + self.flip_pieces_helper(next_column, next_row, player_symbol, direction)
def flip_pieces(self, column, row, player_symbol):
assert self.in_bounds(column, row)
pieces_flipped = 0
for direction in self.DIRECTIONS.keys():
next_column, next_row = self.set_coordinates_in_direction(column, row, direction)
if self.check_endpoint(next_column, next_row, player_symbol, direction, False):
pieces_flipped += self.flip_pieces_helper(next_column, next_row, player_symbol, direction)
return pieces_flipped
def has_legal_moves_remaining(self, player_symbol):
for row in range(self.num_rows):
for column in range(self.num_columns):
if self.cell_empty(column, row) and self.is_legal_move(column, row, player_symbol):
return True
return False
def is_legal_move(self, column, row, player_symbol):
if not self.in_bounds(column, row) or not self.cell_empty(column, row):
return False
for direction in self.DIRECTIONS.keys():
next_column, next_row = self.set_coordinates_in_direction(column, row, direction)
if self.check_endpoint(next_column, next_row, player_symbol, direction, False):
return True
return False
def play_move(self, column, row, player_symbol):
self.set_cell(column, row, player_symbol)
self.flip_pieces(column, row, player_symbol)
def count_score(self, player_symbol):
score = 0
for row in self.grid:
for current_column_value in row:
if current_column_value == player_symbol:
score += 1
return score
class GameDriver:
VALID_PLAYER_TYPES = ["human", "minimax"]
def __init__(self, player_1_type, player_2_type, num_columns, num_rows):
self.player_1 = (HumanPlayer(1, "X") if player_1_type == "human" else MinimaxPlayer(1, "X"))
self.player_2 = (HumanPlayer(2, "O") if player_2_type == "human" else MinimaxPlayer(2, "O"))
self.board = OthelloBoard(num_columns, num_rows, self.player_1.board_symbol, self.player_2.board_symbol)
def display(self):
print()
print("Player 1 ({}) score: {}".format(self.player_1.board_symbol,
self.board.count_score(self.player_1.board_symbol)))
print("Player 2 ({}) score: {}".format(self.player_2.board_symbol,
self.board.count_score(self.player_2.board_symbol)))
self.board.display()
def process_move(self, current_player):
invalid_move = True
while invalid_move:
column, row = current_player.get_move(self.board)
if not self.board.is_legal_move(column, row, current_player.board_symbol):
print("Invalid move")
else:
print("Selected move: col = {}, row = {}".format(column, row))
self.board.play_move(column, row, current_player.board_symbol)
invalid_move = False
def run(self):
game_running = True
current_player = self.player_1
cant_move_counter = 0
while game_running:
print("Player {} ({}) move:".format(current_player.player_number, current_player.board_symbol))
if self.board.has_legal_moves_remaining(current_player.board_symbol):
cant_move_counter = 0
self.process_move(current_player)
self.display()
else:
print("No moves available for player {}".format(current_player.player_number))
cant_move_counter += 1
if cant_move_counter == 2:
game_running = False
current_player = (self.player_1 if current_player == self.player_2 else self.player_2)
player_1_score = self.board.count_score(self.player_1.board_symbol)
player_2_score = self.board.count_score(self.player_2.board_symbol)
print()
if player_1_score == player_2_score:
print("Tie Game")
elif player_1_score > player_2_score:
print("Player 1 Wins")
else:
print("Player 2 Wins")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="A command line version of the Othello board game")
parser.add_argument("player_1_type", choices=["human", "minimax"], help="Type for player 1")
parser.add_argument("player_2_type", choices=["human", "minimax"], help="Type for player 2")
args = parser.parse_args()
game = GameDriver(args.player_1_type, args.player_2_type, 4, 4)
game.run()