From fbf87e723c2b5ba958c6da9b30cbb2e7b13bccf9 Mon Sep 17 00:00:00 2001 From: itay verkh Date: Wed, 25 May 2022 11:07:04 +0300 Subject: [PATCH] full code --- .gitignore | 2 ++ main.py | 18 +++++++++++++++ requirements.txt | 2 ++ src/__init__.py | 0 src/data/__init__.py | 0 src/data/board.py | 21 +++++++++++++++++ src/state/__init__.py | 2 ++ src/state/state_checker.py | 25 ++++++++++++++++++++ src/state/state_type.py | 6 +++++ src/tic_tac_toe_runner.py | 41 +++++++++++++++++++++++++++++++++ src/view/console_board_view.py | 17 ++++++++++++++ tests/test_board.py | 20 ++++++++++++++++ tests/test_state_checker.py | 42 ++++++++++++++++++++++++++++++++++ 13 files changed, 196 insertions(+) create mode 100644 .gitignore create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/data/__init__.py create mode 100644 src/data/board.py create mode 100644 src/state/__init__.py create mode 100644 src/state/state_checker.py create mode 100644 src/state/state_type.py create mode 100644 src/tic_tac_toe_runner.py create mode 100644 src/view/console_board_view.py create mode 100644 tests/test_board.py create mode 100644 tests/test_state_checker.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b960797 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +__pycache__/g \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..fb77780 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +from src.view.console_board_view import ConsoleBoardView +from src.state import StateChecker +from src.tic_tac_toe_runner import TicTacToeRunner +from src.data.board import Board + +def init_board(): + board_size = int(input("choose board size --->")) + return Board(board_size) + +def main(): + state_checker = StateChecker() + board_view = ConsoleBoardView() + runner = TicTacToeRunner(state_checker, board_view, init_board()) + runner.run() + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fc4faad --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +numpy==1.19.5 +pytest==7.1.2 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/data/__init__.py b/src/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/data/board.py b/src/data/board.py new file mode 100644 index 0000000..0523820 --- /dev/null +++ b/src/data/board.py @@ -0,0 +1,21 @@ +import numpy as np +from src.state import State + +class Board: + def __init__(self, size = 3): + self._matrix = np.zeros((size, size), int) + self.size = size + + def fill_cell(self, index: tuple, state: State): + self._matrix[index[0], index[1]] = state.value + + def get_matrix(self): + return self._matrix + + def is_valid_coordinates(self, coordinates: (int, int)): + try: + if (self._matrix[coordinates[0], coordinates[1]] == State.Empty.value): + return True + return False + except KeyError: + return False \ No newline at end of file diff --git a/src/state/__init__.py b/src/state/__init__.py new file mode 100644 index 0000000..8aaed18 --- /dev/null +++ b/src/state/__init__.py @@ -0,0 +1,2 @@ +from .state_type import * +from .state_checker import * \ No newline at end of file diff --git a/src/state/state_checker.py b/src/state/state_checker.py new file mode 100644 index 0000000..f646cf4 --- /dev/null +++ b/src/state/state_checker.py @@ -0,0 +1,25 @@ +from .state_type import State +import numpy as np + + +class StateChecker: + + def is_won(self, board): + board_matrix = board.get_matrix() + board_matrix_t = np.transpose(board_matrix) + return self._scan_rows(board_matrix) or self._scan_rows(board_matrix_t) \ + or self._scan_diagonals(board_matrix) + + def _scan_rows(self, matrix): + for row in matrix: + # this if checks whether the entire row has the same value and its not empty + if (len(set(row)) == 1 and row[0] != State.Empty.value): + return True + return False + + def _scan_diagonals(self, matrix): + is_diag_win = self._scan_rows([np.diag(matrix)]) + if (not is_diag_win): + # the secondary diagonal + is_diag_win = self._scan_rows([np.diag(np.rot90(matrix))]) + return is_diag_win diff --git a/src/state/state_type.py b/src/state/state_type.py new file mode 100644 index 0000000..7f75931 --- /dev/null +++ b/src/state/state_type.py @@ -0,0 +1,6 @@ +from enum import Enum + +class State(Enum): + Empty = 0 + X = 1 + O = 2 \ No newline at end of file diff --git a/src/tic_tac_toe_runner.py b/src/tic_tac_toe_runner.py new file mode 100644 index 0000000..1c219a6 --- /dev/null +++ b/src/tic_tac_toe_runner.py @@ -0,0 +1,41 @@ +from src.data.board import Board +from src.state import StateChecker, State +from src.view.console_board_view import ConsoleBoardView + + +class TicTacToeRunner: + def __init__(self, state_checker: StateChecker, board_view: ConsoleBoardView, + board: Board): + self.state_checker = state_checker + self.board_view = board_view + self.board = board + + def run(self): + turns_elapsed = 0 + max_turns = self.board.size ** 2 + while turns_elapsed < max_turns: + self.board_view.show_board(self.board) + user_turn = ('X', State.X) if turns_elapsed % 2 == 0 else ('O', State.O) + coordinates = self._get_user_input(user_turn) + self.board.fill_cell(coordinates, user_turn[1]) + has_finished = self.state_checker.is_won(self.board) + if has_finished: + print(F"its a win for {user_turn[0]}!") + return + turns_elapsed += 1 + print("this game ends with a tie") + + def _get_user_input(self, whos_turn) -> (int, int): + while True: + try: + coordinates_raw = input(F"{whos_turn[0]} turn, select coordinates ") + split = coordinates_raw.split(',') + coordinates = int(split[0]), int(split[1]) + if (self.board.is_valid_coordinates(coordinates)): + return coordinates + else: + raise Exception("invalid coordinates, try again") + except KeyError or ValueError: + print("please input coordinates in the following format: x,y") + except Exception as e: + print(e) diff --git a/src/view/console_board_view.py b/src/view/console_board_view.py new file mode 100644 index 0000000..4a691a7 --- /dev/null +++ b/src/view/console_board_view.py @@ -0,0 +1,17 @@ +from src.data.board import Board + +class ConsoleBoardView: + + def show_board(self, board: Board): + vertical_border = '| ' + self.__print_horizontal_border(len(board.get_matrix())) + for row in board.get_matrix(): + print(vertical_border , end="") + for cell in row: + print(" " if cell == 0 else 'X' if cell == 1 else 'O', vertical_border, end="") + print() + self.__print_horizontal_border(len(board.get_matrix())) + + def __print_horizontal_border(self, length): + horizontal_border = ' --- ' + print(horizontal_border * length) \ No newline at end of file diff --git a/tests/test_board.py b/tests/test_board.py new file mode 100644 index 0000000..7e9d2ac --- /dev/null +++ b/tests/test_board.py @@ -0,0 +1,20 @@ +from src.data.board import Board +from src.state import State + + +class TestBoard: + board = Board() + + def test_fill_cell_updates_correct_cell(self): + self.board.fill_cell((0, 0), State.O) + assert self.board.get_matrix()[0][0] == State.O.value + + def test_board_is_valid_coordinates_false_on_taken_cell(self): + coordinates = (0, 0) + self.board.fill_cell(coordinates, State.X) + assert self.board.is_valid_coordinates(coordinates) == False + + def test_board_is_valid_coordinates_true_on_empty_cell(self): + coordinates = (0, 0) + self.board.fill_cell(coordinates, State.X) + assert self.board.is_valid_coordinates((0, 1)) == True \ No newline at end of file diff --git a/tests/test_state_checker.py b/tests/test_state_checker.py new file mode 100644 index 0000000..e538415 --- /dev/null +++ b/tests/test_state_checker.py @@ -0,0 +1,42 @@ +from src.state import State, StateChecker +from unittest.mock import Mock + +class TestStateChecker: + state_checker = StateChecker() + board_mock = Mock() + + def test_is_won_returns_false_on_empty_board(self): + game_matrix = [[State.Empty.value for j in range(3)] for i in range(3)] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == False + + def test_is_won_returns_true_on_row_of_x(self): + game_matrix = [[State.X.value for j in range(3)] for i in range(1)] \ + + [[State.Empty.value for k in range(2)]] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == True + + def test_is_won_returns_true_on_row_of_o(self): + game_matrix = [[State.O.value for j in range(3)] for i in range(1)] \ + + [[State.Empty.value for k in range(2)]] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == True + + def test_is_won_returns_true_on_column_of_x(self): + game_matrix = [[State.X.value, State.Empty.value, State.Empty.value] for i in range(3)] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == True + + def test_is_won_returns_true_on_diagonal_of_x(self): + game_matrix = [[State.X.value, State.Empty.value, State.Empty.value], + [State.O.value, State.X.value, State.Empty.value], + [State.O.value, State.Empty.value, State.X.value]] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == True + + def test_is_won_returns_true_on_secondary_diagonal_of_x(self): + game_matrix = [[State.O.value, State.Empty.value, State.X.value], + [State.O.value, State.X.value, State.Empty.value], + [State.X.value, State.Empty.value, State.O.value]] + self.board_mock.get_matrix.return_value = game_matrix + assert self.state_checker.is_won(self.board_mock) == True \ No newline at end of file