diff --git a/board.py b/board.py index 1667d88..ddc741f 100644 --- a/board.py +++ b/board.py @@ -1,122 +1,111 @@ -from collections import deque - - -PLAYER_X = 1 -PLAYER_O = -1 -NO_PLAYER = 0 - -STR_MATRIX = { - PLAYER_X: 'X', - PLAYER_O: 'O', - NO_PLAYER: '-' -} - -ROWS = 3 -BOARD_SIZE = ROWS*ROWS - -LOSS = 0.0 -DRAW = 0.5 -WIN = 1.0 - - -class BaseBoard: - """Defines the general structure which a board implementation must implement""" - def __init__(self): - raise NotImplementedError - - def __str__(self): - raise NotImplementedError - - def __copy__(self): - raise NotImplementedError - - def make_move(self, move): - raise NotImplementedError - - def take_move(self): - raise NotImplementedError - - def get_moves(self): - raise NotImplementedError - - def get_result(self, player_jm): - raise NotImplementedError - - -class Board: - def __init__(self): - self.pos = [0] * BOARD_SIZE - self.side = PLAYER_X - self.playerJustMoved = PLAYER_O - self.history = deque() - - def __str__(self): - lines = [] - for combo in zip(*[self.pos[i::ROWS] for i in range(ROWS)]): - lines.extend(['{:<5}'.format(STR_MATRIX[elem]) for elem in combo]) - lines.append('\n') - return ''.join(lines) - - def __copy__(self): - _b = Board() - _b.pos = self.pos[:] # copy list - _b.side = self.side # todo remove this, not needed since player just moved - _b.playerJustMoved = self.playerJustMoved - _b.history = self.history.copy() # todo copying deque is too slow - return _b - - def make_move(self, move): - assert move in self.get_moves(), 'Position is already occupied' - - self.pos[move] = self.side - self.side = -self.side # change side to move - self.playerJustMoved = -self.playerJustMoved - self.history.append(move) - - def take_move(self): - move = self.history.pop() - self.pos[move] = NO_PLAYER - self.side = -self.side # change side to move - self.playerJustMoved = -self.playerJustMoved - - def get_moves(self): - return [idx for idx, value in enumerate(self.pos) if value == NO_PLAYER] - - def get_result(self, player_jm): - cols_combo = [self.pos[i::ROWS] for i in range(ROWS)] - rows_combo = list(zip(*cols_combo)) - # print(cols_combo) - # print(row s_combo) - - for i in range(ROWS): - # Sum a row and a column - row_result, col_result = sum(rows_combo[i]), sum(cols_combo[i]) - - # Check if sum of values of a row is not equal to number of rows i.e. all 1s or all -1s - if abs(row_result) == ROWS: - return WIN if int(row_result / ROWS) == player_jm else LOSS - - if abs(col_result) == ROWS: - return WIN if int(col_result / ROWS) == player_jm else LOSS - - # Sum values on Right diagonal - # Look at right Diagonal - # exclude last element since it is not part of the diagonal - # i.e. if you have [1, 2, 3, - # 4, 5, 6, - # 7 ,8 ,9] then right diagonal is [3, 5, 7] - # i.e. starting from the right corner the diagonal is formed by every second number - # (3, 5, 7), however this will also result in 9 being included which it should not be - # therefore we remove it - result = sum(self.pos[ROWS - 1::ROWS - 1][:-1]) - if abs(result) == ROWS: - return WIN if int(result / ROWS) == player_jm else LOSS - - # Left diagonal - result = sum(self.pos[::ROWS + 1]) - if abs(result) == ROWS: - return WIN if int(result / ROWS) == player_jm else LOSS - - # Lastly check if no available squares are on the board => TIE - if sum([abs(elem) for elem in self.pos]) == BOARD_SIZE: - return DRAW +PLAYER_X = 1 +PLAYER_O = -1 +NO_PLAYER = 0 + +STR_MATRIX = { + PLAYER_X: 'X', + PLAYER_O: 'O', + NO_PLAYER: '-' +} + +ROWS = 3 +BOARD_SIZE = ROWS*ROWS + +LOSS = 0.0 +DRAW = 0.5 +WIN = 1.0 + + +class BaseBoard: + """Defines the general structure which a board implementation + must implement + """ + + def __str__(self): + raise NotImplementedError + + def __copy__(self): + raise NotImplementedError + + def make_move(self, move): + raise NotImplementedError + + def take_move(self): + raise NotImplementedError + + def get_moves(self): + raise NotImplementedError + + def get_result(self, player_jm): + raise NotImplementedError + + +class Board: + def __init__(self): + self.pos = [0] * BOARD_SIZE + + def get_row_strings(self): + lines = [] + for combo in zip(*[self.pos[i::ROWS] for i in range(ROWS)]): + lines.append(''.join(['{:<5}'.format(STR_MATRIX[elem]) for elem in combo])) + return lines + + def __copy__(self): + _b = Board() + _b.pos = self.pos[:] # copy list + return _b + + def make_move(self, move, side): + assert move in self.get_moves(), 'Position is already occupied' + + self.pos[move] = side + + def take_move(self, move): + self.pos[move] = NO_PLAYER + + def get_moves(self): + return [idx for idx, value in enumerate(self.pos) if value == NO_PLAYER] + + def get_result(self, board=None): + if board is None: + board = self.pos + + cols_combo = [board[i::ROWS] for i in range(ROWS)] + rows_combo = list(zip(*cols_combo)) + + for i in range(ROWS): + # Sum a row and a column + row_result, col_result = sum(rows_combo[i]), sum(cols_combo[i]) + + # Check if sum of values of a row is not equal to number of rows + # i.e. all 1s or all -1s + if abs(row_result) == ROWS: + return int(row_result / ROWS) + + if abs(col_result) == ROWS: + return int(col_result / ROWS) + + # Sum values on Right diagonal + # Look at right Diagonal + # exclude last element since it is not part of the diagonal + # i.e. if you have [1, 2, 3, + # 4, 5, 6, + # 7 ,8 ,9] then right diagonal is [3, 5, 7] + # i.e. starting from the right corner the diagonal is formed by + # every second number (3, 5, 7), however this will also result + # in 9 being included which it should not be therefore we remove it + result = sum(board[ROWS - 1::ROWS - 1][:-1]) + if abs(result) == ROWS: + return int(result / ROWS) + + # Left diagonal + result = sum(board[::ROWS + 1]) + if abs(result) == ROWS: + return int(result / ROWS) + + # Lastly check if no available squares are on the board => TIE + if sum([abs(elem) for elem in board]) == BOARD_SIZE: + # here 0.5 indicates a DRAW and for ultimate tttoe + # this means that a drawn board is not taken into account for + # any player + return DRAW diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..bdae3d9 --- /dev/null +++ b/gui.py @@ -0,0 +1,89 @@ +# tutorial from https://pythonprogramming.net/drawing-objects-pygame-tutorial/?completed=/displaying-text-pygame-screen/ + +import pygame +import time + +pygame.init() + +display_width = 800 +display_height = 600 + +black = (0, 0, 0) +white = (255, 255, 255) +red = (255, 0, 0) + +car_width = 73 + +gameDisplay = pygame.display.set_mode((display_width, display_height)) +pygame.display.set_caption('A bit Racey') +clock = pygame.time.Clock() + +carImg = pygame.image.load('racecar.png') + + +def car(x, y): + gameDisplay.blit(carImg, (x, y)) + + +def text_objects(text, font: pygame.font.Font): + textSurface = font.render(text, True, black) + return textSurface, textSurface.get_rect() + + +def message_display(text): + largeText = pygame.font.Font('freesansbold.ttf', 115) + TextSurf, TextRect = text_objects(text, largeText) + TextRect.center = ((display_width / 2), (display_height / 2)) + gameDisplay.blit(TextSurf, TextRect) + + pygame.display.update() + + +def crash(): + message_display('You Crashed') + time.sleep(2) + + game_loop() + + +def game_loop(): + x = (display_width * 0.45) + y = (display_height * 0.8) + + x_change = 0 + + gameExit = False + + while not gameExit: + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + quit() + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_LEFT: + x_change = -5 + if event.key == pygame.K_RIGHT: + x_change = 5 + + if event.type == pygame.KEYUP: + if event.key == pygame.K_LEFT or event.key == pygame.K_RIGHT: + x_change = 0 + + x += x_change + + gameDisplay.fill(white) + car(x, y) + + if x > display_width - car_width or x < 0: + crash() + + pygame.display.update() + clock.tick(60) + + +game_loop() +pygame.quit() +quit() + diff --git a/racecar.png b/racecar.png new file mode 100644 index 0000000..4a2d567 Binary files /dev/null and b/racecar.png differ diff --git a/ultimate_board.py b/ultimate_board.py index d3f8fea..a96fe24 100644 --- a/ultimate_board.py +++ b/ultimate_board.py @@ -1,24 +1,163 @@ -from ultimate_tttoe.board import * - - -class UltimateBoard(BaseBoard): - def __init__(self): - pass - - def __str__(self): - pass - - def __copy__(self): - pass - - def make_move(self, move): - pass - - def take_move(self): - pass - - def get_moves(self): - pass - - def get_result(self, player_jm): - pass +import random +from collections import deque, namedtuple +from pprint import pprint +from typing import List, Tuple + +from board import * + + +Move = namedtuple('Move', ['board_idx', 'move_idx']) + + +class UltimateBoard(BaseBoard): + def __init__(self): + self.pos = [Board() for _ in range(BOARD_SIZE)] + # indicates where (which board) the next move should be done + self.nextBoard = None + self.playerJustMoved = PLAYER_O + self.history = deque() + + def __str__(self): + lines = [] + for combo in zip(*[self.pos[i::ROWS] for i in range(ROWS)]): + rows = zip(*[elem.get_row_strings() for elem in combo]) + row = [''.join(['\t|\t'.join(row), '\n']) for row in rows] + row_length = sum([len(r) for r in row]) // ROWS + lines.extend(row) + lines.append(''.join(['_'*row_length, '\n'])) + return ''.join(lines) + + def __copy__(self): + _b = UltimateBoard() + _b.pos = [board.__copy__() for board in self.pos] + _b.nextBoard = self.nextBoard + _b.playerJustMoved = self.playerJustMoved + _b.history = self.history.copy() + return _b + + def make_move(self, move: int, board: int = None): + """Make a move to the specified board, if no board is specified + the move is done on the board indicated by nextBoard i.e forced + by the last played move. + + @param move: move to play (i.e. index on the board) + @param board: index indicating which board to play the move on + """ + self.playerJustMoved = -self.playerJustMoved + + if self.nextBoard is None and board is None: + print('Need to provide a board idx at start of game') + return + + if self.nextBoard is not None: + # if we are forced to play on a board (due to opponents move) + # check if nextBoard has a winner. If it does => player should + # have picked a new board to play on + if self.pos[self.nextBoard].get_result() is not None: + if board is None: + print('The forced board already has a result on it.' + 'Please choose a different board.') + return + else: + if self.pos[board].get_result() is not None: + print('The chosen board has a result on it.' + 'Please chose a different board.') + return + + self.pos[board].make_move(move, self.playerJustMoved) + self.history.append(Move(board_idx=board, move_idx=move)) + self.nextBoard = move # the move on the board represents the next board + return + + # otherwise the forced board, doesn't have a result and the new move + # must be played on it + self.pos[self.nextBoard].make_move(move, self.playerJustMoved) + self.history.append(Move(board_idx=self.nextBoard, move_idx=move)) + self.nextBoard = move + return + + # if board is not None but nextBoard is None (i.e. start of game) + self.pos[board].make_move(move, self.playerJustMoved) + self.history.append(Move(board_idx=board, move_idx=move)) + self.nextBoard = move + + def take_move(self): + move = self.history.pop() + self.pos[move.board_idx].take_move(move.move_idx) + # todo need to update self.nextBoard + + def get_all_moves(self) -> List[Tuple[int, int]]: + """Get all possible moves in the form of a list of tuples + containing (Board index, Move index). + """ + all_moves = [] + for idx, board in enumerate(self.pos): + if board.get_result() is not None: + continue + + moves = board.get_moves() + if moves: + all_moves.extend(zip([idx]*len(moves), moves)) + return all_moves + + def get_moves(self): + """If a board is forced, return only the moves from that board. + If a board is forced but that board already has a result, + return all possible moves from all the other boards. If no + board is forced, return all possible moves. + """ + # at the start of the game return all possible moves + if self.nextBoard is None: + return self.get_all_moves() + + # if a forced board has a result -> return list of moves for + # all other boards that do not yet have a result + if self.pos[self.nextBoard].get_result() is not None: + all_moves = [] + for idx, board in enumerate(self.pos): + if idx != self.nextBoard and board.get_result() is None: + moves = board.get_moves() + if moves: + all_moves.extend(zip([idx] * len(moves), moves)) + return all_moves + + # if forced board doesn't have a result yet => get all possible moves + # from that board + all_moves = [] + moves = self.pos[self.nextBoard].get_moves() + if moves: + all_moves.extend(zip([self.nextBoard] * len(moves), moves)) + + return all_moves + + def get_result(self, player_jm): # todo use player_jm instead of self. + # build a list containing the results from each individual board + # where there is no result i.e. None => replace with 0 + result_board = [board.get_result() for board in self.pos] + result_board = [0 if result is None else result for result in result_board] + + # find the result based on the result board + # (use an arbitrary instance of Board() to compute the result) + result = self.pos[0].get_result(board=result_board) + + if result not in (self.playerJustMoved, -self.playerJustMoved) and not self.get_all_moves(): + # if there is no winner and no available moves => DRAW + return DRAW + elif result in (self.playerJustMoved, -self.playerJustMoved): + return WIN if result == self.playerJustMoved else LOSS + + +if __name__ == '__main__': + ub = UltimateBoard() + print(ub) + print('\n\n') + player = -ub.playerJustMoved + while ub.get_result(ub.playerJustMoved) is None: + moves = ub.get_moves() + board, move = random.choice(moves) + print(board, move) + ub.make_move(move, board) + print(ub) + print('\n\n') + + print('Game finished. Result is', ub.get_result(player))