diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2e5f73..17332c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,6 +66,8 @@ repos: args: [ "-rn", # Only display messages "-sn", # Don't display the score + "--rcfile", + "tictactoe_python/pyproject.toml", ] # golang specific diff --git a/tictactoe_python/pyproject.toml b/tictactoe_python/pyproject.toml index dc89f35..ebd6ced 100644 --- a/tictactoe_python/pyproject.toml +++ b/tictactoe_python/pyproject.toml @@ -43,7 +43,27 @@ target-version = "py311" [tool.ruff.lint] # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. select = ["ALL"] -ignore = ["ANN101", "S311", "T20", "COM812", "ISC001"] +ignore = [ + # Conflict with formatter + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", + # Others + "ANN101", + "S311", + "T20", + "E501", +] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" @@ -79,3 +99,101 @@ reportAssertAlwaysTrue = "error" reportUnnecessaryTypeIgnoreComment = "error" reportImplicitOverride = "error" reportShadowedImports = "error" + +[tool.pylint.main] +# Specify a score threshold under which the program will exit with error. +fail-under = 10.0 + +[tool.pylint.basic] +# Good variable names which should always be accepted, separated by a comma. +good-names = ["i", "j", "k", "ex", "Run", "_", "x", "y", "z", "e", "ok"] +include-naming-hint = true +typevar-rgx = "^_{0,2}(?!T[A-Z])([A-Z]|T[0-9]?)(?:[A-Z]*[0-9]*)?(?:_co(?:ntra)?)?$" + +[tool.pylint.design] +# Maximum number of arguments for function / method. +max-args = 10 + +# Maximum number of attributes for a class (see R0902). +# Should be lowered to 7 but got no time for that atm +max-attributes = 15 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr = 5 + +# Maximum number of branch for function / method body. +# Same as for max-attributes +max-branches = 12 + +# Maximum number of locals for function / method body. +# Same as for max-attributes +max-locals = 15 + +# Maximum number of public methods for a class (see R0904). +max-public-methods = 20 + +# Maximum number of return / yield for function / method body. +max-returns = 6 + +# Maximum number of statements in function / method body. +max-statements = 60 + +# Minimum number of public methods for a class (see R0903). +# Same as for max-attributes +min-public-methods = 2 + +[tool.pylint.exceptions] +# Exceptions that will emit a warning when caught. +overgeneral-exceptions = ["builtins.BaseException"] + +[tool.pylint.format] +# Maximum number of characters on a single line. +max-line-length = 120 + +# Maximum number of lines in a module. +max-module-lines = 2000 + +[tool.pylint."messages control"] +# Only show warnings with the listed confidence levels. Leave empty to show all. +# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] + +# Disable the message, report, category or checker with the given id(s). You can +# either give multiple identifiers separated by comma (,) or put this option +# multiple times (only on the command line, not in the configuration file where +# it should appear only once). You can also use "--disable=all" to disable +# everything first and then re-enable specific checks. For example, if you want +# to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable = ["unnecessary-ellipsis"] # but needed for pyright + +[tool.pylint.miscellaneous] +# List of note tags to take in consideration, separated by a comma. +notes = ["FIXME", "XXX", "TODO"] + +[tool.pylint.refactoring] +# Maximum number of nested blocks for function / method body +max-nested-blocks = 5 + +# Complete name of functions that never returns. When checking for inconsistent- +# return-statements if a never returning function is called then it will be +# considered as an explicit return statement and no message will be printed. +never-returning-functions = ["sys.exit", "argparse.parse_error"] + +[tool.pylint.reports] +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each category, +# as well as 'statement' which is the total number of statements analyzed. This +# score is used by the global evaluation report (RP0004). +evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))" + +[tool.pylint.similarities] +# Minimum lines number of a similarity. +min-similarity-lines = 20 + +[tool.pylint.spelling] +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions = 4 diff --git a/tictactoe_python/requirements.txt b/tictactoe_python/requirements.txt index e69de29..85c5cde 100644 --- a/tictactoe_python/requirements.txt +++ b/tictactoe_python/requirements.txt @@ -0,0 +1 @@ +typing-extensions<=4.4.0 diff --git a/tictactoe_python/tests/conftest.py b/tictactoe_python/tests/conftest.py new file mode 100644 index 0000000..4d9d0be --- /dev/null +++ b/tictactoe_python/tests/conftest.py @@ -0,0 +1,15 @@ +"""Fixtures for tictactoe tests.""" + +import pytest + +from tictactoe.tictactoe_python import TicTacToe + + +@pytest.fixture +def tictactoe() -> TicTacToe: + """Return a TicTacToe instance. + + Returns: + TicTacToe: Return a freashly created TicTacToe instance. + """ + return TicTacToe() diff --git a/tictactoe_python/tests/requirements.txt b/tictactoe_python/tests/requirements.txt index cced0ee..93e3ea4 100644 --- a/tictactoe_python/tests/requirements.txt +++ b/tictactoe_python/tests/requirements.txt @@ -1,5 +1,5 @@ pre-commit==3.8.0 pylint==3.2.7 -pyright==1.1.378 +pyright==1.1.379 pytest==8.3.2 pytest-cov==5.0.0 diff --git a/tictactoe_python/tests/test_tictactoe.py b/tictactoe_python/tests/test_tictactoe.py index ba5553d..63ae029 100644 --- a/tictactoe_python/tests/test_tictactoe.py +++ b/tictactoe_python/tests/test_tictactoe.py @@ -2,439 +2,507 @@ from unittest.mock import MagicMock, patch -from tictactoe.tictactoe_python import TicTacToe - - -class TestTicTacToe: - """Class to test TicTacToe.""" - - def setup_class(self): - """Setup class getting a TicTacToe object.""" - self.tictactoe = TicTacToe() - - def teardown_class(self): - """Set tictactoe to None.""" - self.tictactoe = None - - def test_init(self): - """Tests initialization.""" - assert isinstance(self.tictactoe.board, list) - assert not self.tictactoe.board - assert isinstance(self.tictactoe.empty_indicator, str) - assert self.tictactoe.empty_indicator == "-" - - def test_create_board(self): - """Tests create_board.""" - self.tictactoe.create_board() - assert len(self.tictactoe.board) == 3 - assert isinstance(self.tictactoe.board[0], list) - assert isinstance(self.tictactoe.board[0][0], str) - assert self.tictactoe.board[0][0] == self.tictactoe.empty_indicator - assert self.tictactoe.board == [ - [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - ], - [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - ], - [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - ], - ] - - def test_fix_spot(self): - """Tests fix_spot.""" - assert not self.tictactoe.fix_spot(-1, -1, "X") - assert not self.tictactoe.fix_spot(3, 3, "X") - assert self.tictactoe.fix_spot(1, 1, "X") - assert self.tictactoe.board[1][1] == "X" - assert not self.tictactoe.fix_spot(1, 1, "X") - - def test_is_player_win(self): - """Tests is_player_win.""" - # Row win X - board = [["X", "X", "X"], ["-", "-", "-"], ["-", "-", "-"]] - assert self.tictactoe.is_player_win("X", board) - assert not self.tictactoe.is_player_win("O", board) - # Row no win - board = [["X", "X", "O"], ["-", "-", "-"], ["-", "-", "-"]] - assert not self.tictactoe.is_player_win("X", board) - assert not self.tictactoe.is_player_win("O", board) - # Col win X - board = [["X", "O", "O"], ["X", "-", "-"], ["X", "-", "-"]] - assert self.tictactoe.is_player_win("X", board) - assert not self.tictactoe.is_player_win("O", board) - # Col no win - board = [["X", "O", "O"], ["X", "-", "-"], ["O", "-", "-"]] - assert not self.tictactoe.is_player_win("X", board) - assert not self.tictactoe.is_player_win("O", board) - # Diagonal win O - board = [["O", "X", "O"], ["X", "O", "-"], ["X", "-", "O"]] - assert self.tictactoe.is_player_win("O", board) - assert not self.tictactoe.is_player_win("X", board) - # Diagonal no win - board = [["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]] - assert not self.tictactoe.is_player_win("O", board) - assert not self.tictactoe.is_player_win("X", board) - # Antidiagonal win O - board = [["O", "X", "O"], ["X", "O", "-"], ["O", "O", "-"]] - assert self.tictactoe.is_player_win("O", board) - assert not self.tictactoe.is_player_win("X", board) - # Antidiagonal no win - board = [["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]] - assert not self.tictactoe.is_player_win("O", board) - assert not self.tictactoe.is_player_win("X", board) - - def test_is_board_filled(self): - """Tests is_board_filled.""" - self.tictactoe.board = [["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]] - assert self.tictactoe.is_board_filled() - self.tictactoe.board = [ - [self.tictactoe.empty_indicator, "X", "X"], +from tictactoe.tictactoe_python import Board, EndState, Move, TicTacToe + + +def test_init(tictactoe: TicTacToe): + """Tests initialization. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + assert isinstance(tictactoe.board, Board) + assert isinstance(tictactoe.empty_indicator, str) + assert tictactoe.empty_indicator == "-" + + +def test_create_board(tictactoe: TicTacToe): + """Tests create_board. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + assert len(tictactoe.board) == 3 + assert isinstance(tictactoe.board[0, 0], str) + assert tictactoe.board[0, 0] == tictactoe.empty_indicator + assert tictactoe.board._board == [ + [ + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + ], + [ + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + ], + [ + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + ], + ] + + +def test_fix_spot(tictactoe: TicTacToe): + """Tests fix_spot. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + assert not tictactoe.fix_spot(-1, -1, "X") + assert not tictactoe.fix_spot(3, 3, "X") + assert tictactoe.fix_spot(1, 1, "X") + assert tictactoe.board[1, 1] == "X" + assert not tictactoe.fix_spot(1, 1, "X") + + +def test_is_player_win(tictactoe: TicTacToe): + """Tests is_player_win. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + # Row win X + board = Board([["X", "X", "X"], ["-", "-", "-"], ["-", "-", "-"]]) + assert tictactoe.is_player_win("X", board) + assert not tictactoe.is_player_win("O", board) + # Row no win + board = Board([["X", "X", "O"], ["-", "-", "-"], ["-", "-", "-"]]) + assert not tictactoe.is_player_win("X", board) + assert not tictactoe.is_player_win("O", board) + # Col win X + board = Board([["X", "O", "O"], ["X", "-", "-"], ["X", "-", "-"]]) + assert tictactoe.is_player_win("X", board) + assert not tictactoe.is_player_win("O", board) + # Col no win + board = Board([["X", "O", "O"], ["X", "-", "-"], ["O", "-", "-"]]) + assert not tictactoe.is_player_win("X", board) + assert not tictactoe.is_player_win("O", board) + # Diagonal win O + board = Board([["O", "X", "O"], ["X", "O", "-"], ["X", "-", "O"]]) + assert tictactoe.is_player_win("O", board) + assert not tictactoe.is_player_win("X", board) + # Diagonal no win + board = Board([["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]]) + assert not tictactoe.is_player_win("O", board) + assert not tictactoe.is_player_win("X", board) + # Antidiagonal win O + board = Board([["O", "X", "O"], ["X", "O", "-"], ["O", "O", "-"]]) + assert tictactoe.is_player_win("O", board) + assert not tictactoe.is_player_win("X", board) + # Antidiagonal no win + board = Board([["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]]) + assert not tictactoe.is_player_win("O", board) + assert not tictactoe.is_player_win("X", board) + + +def test_is_board_filled(tictactoe: TicTacToe): + """Tests is_board_filled. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + tictactoe.board = Board([["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]]) + assert tictactoe.is_board_filled() + tictactoe.board = Board( + [ + [tictactoe.empty_indicator, "X", "X"], ["X", "X", "X"], ["X", "X", "X"], ] - assert not self.tictactoe.is_board_filled() - self.tictactoe.board = [ - [self.tictactoe.empty_indicator, "X", "X"], + ) + assert not tictactoe.is_board_filled() + tictactoe.board = Board( + [ + [tictactoe.empty_indicator, "X", "X"], ["X", "X", "X"], ["X", "X", "X"], ] - assert not self.tictactoe.is_board_filled() - self.tictactoe.board = [ + ) + assert not tictactoe.is_board_filled() + tictactoe.board = Board( + [ ["X", "X", "X"], - ["X", self.tictactoe.empty_indicator, "X"], + ["X", tictactoe.empty_indicator, "X"], ["X", "X", "X"], ] - assert not self.tictactoe.is_board_filled() - self.tictactoe.board = [ + ) + assert not tictactoe.is_board_filled() + tictactoe.board = Board( + [ ["X", "X", "X"], ["X", "X", "X"], - ["X", "X", self.tictactoe.empty_indicator], + ["X", "X", tictactoe.empty_indicator], ] - assert not self.tictactoe.is_board_filled() - - def test_swap_player_turn(self): - """Tests swap_player_turn.""" - assert self.tictactoe.swap_player_turn("X") == "O" - assert self.tictactoe.swap_player_turn("O") == "X" - - @patch("builtins.print") - def test_show_board(self, print_mock: MagicMock): - """Tests show_board. - - Args: - print_mock (MagicMock): Mock out print - """ - self.tictactoe.show_board() - assert print_mock.call_count == 3 * 3 + 3 + 1 - - @patch("builtins.input") - def test_get_player_input(self, input_mock: MagicMock): - """Tests get_player_input. - - Args: - input_mock (MagicMock): Mock out user input - """ - input_mock.side_effect = ["1 0", "1", "", "1 1 1", "-1 1", "3 1", "a 1", "2 2"] - assert self.tictactoe.get_player_input() == (1, 0) - assert self.tictactoe.get_player_input() is None - assert self.tictactoe.get_player_input() is None - assert self.tictactoe.get_player_input() is None - assert self.tictactoe.get_player_input() == (-1, 1) - assert self.tictactoe.get_player_input() == (3, 1) - assert self.tictactoe.get_player_input() is None - assert self.tictactoe.get_player_input() == (2, 2) - - def test_empty_cells(self): - """Tests empty_cells.""" - board = [["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]] - assert self.tictactoe.empty_cells(board) == [] - board = [ - ["X", "O", self.tictactoe.empty_indicator], - ["O", self.tictactoe.empty_indicator, "X"], - [self.tictactoe.empty_indicator, "X", "X"], + ) + assert not tictactoe.is_board_filled() + + +def test_swap_player_turn(tictactoe: TicTacToe): + """Tests swap_player_turn. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + assert tictactoe.swap_player_turn("X") == "O" + assert tictactoe.swap_player_turn("O") == "X" + + +@patch("builtins.input") +def test_get_player_input(input_mock: MagicMock, tictactoe: TicTacToe): + """Tests get_player_input. + + Args: + input_mock (MagicMock): Mock of the input function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + input_mock.side_effect = ["1 0", "1", "", "1 1 1", "-1 1", "3 1", "a 1", "2 2"] + assert tictactoe.get_player_input() == (1, 0) + assert tictactoe.get_player_input() is None + assert tictactoe.get_player_input() is None + assert tictactoe.get_player_input() is None + assert tictactoe.get_player_input() == (-1, 1) + assert tictactoe.get_player_input() == (3, 1) + assert tictactoe.get_player_input() is None + assert tictactoe.get_player_input() == (2, 2) + + +def test_empty_cells(tictactoe: TicTacToe): + """Tests empty_cells. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + board = Board([["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]]) + assert tictactoe.empty_cells(board) == [] + board = Board( + [ + ["X", "O", tictactoe.empty_indicator], + ["O", tictactoe.empty_indicator, "X"], + [tictactoe.empty_indicator, "X", "X"], ] - assert self.tictactoe.empty_cells(board) == [(0, 2), (1, 1), (2, 0)] - - @patch("builtins.input") - def test_get_player_number(self, input_mock: MagicMock): - """Tests get_player_number. - - Args: - input_mock (MagicMock): Mock out user input - """ - input_mock.side_effect = ["y", "n", "", "Y", 1, "N"] - self.tictactoe.get_player_number() - assert self.tictactoe.ai_opponent is True - assert input_mock.call_count == 1 - self.tictactoe.get_player_number() - assert self.tictactoe.ai_opponent is False - assert input_mock.call_count == 2 - self.tictactoe.get_player_number() - assert self.tictactoe.ai_opponent is True - assert input_mock.call_count == 4 - self.tictactoe.get_player_number() - assert self.tictactoe.ai_opponent is False - assert input_mock.call_count == 6 - - @patch("builtins.input") - def test_get_ai_start(self, input_mock: MagicMock): - """Tests get_ai_start. - - Args: - input_mock (MagicMock): Mock out user input - """ - input_mock.side_effect = ["y", "n", "", "Y", 1, "N"] - self.tictactoe.get_ai_start() - assert self.tictactoe.ai_marker == "X" - assert input_mock.call_count == 1 - self.tictactoe.get_ai_start() - assert self.tictactoe.ai_marker == "O" - assert input_mock.call_count == 2 - self.tictactoe.get_ai_start() - assert self.tictactoe.ai_marker == "X" - assert input_mock.call_count == 4 - self.tictactoe.get_ai_start() - assert self.tictactoe.ai_marker == "O" - assert input_mock.call_count == 6 - - @patch("builtins.input") - def test_get_ai_strength(self, input_mock: MagicMock): - """Tests get_ai_start. - - Args: - input_mock (MagicMock): Mock out user input - """ - input_mock.side_effect = ["1", "2", "", "3", "X", "4"] - self.tictactoe.get_ai_strength() - assert self.tictactoe.ai_function == self.tictactoe.random_move - assert input_mock.call_count == 1 - self.tictactoe.get_ai_strength() - assert self.tictactoe.ai_function == self.tictactoe.win_move - assert input_mock.call_count == 2 - self.tictactoe.get_ai_strength() - assert self.tictactoe.ai_function == self.tictactoe.block_win_move - assert input_mock.call_count == 4 - self.tictactoe.get_ai_strength() - assert self.tictactoe.ai_function == self.tictactoe.minmax - assert input_mock.call_count == 6 - - def test_minmax(self): - """Tests minmax.""" - board = [["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]] - assert self.tictactoe.minmax(board, "X") == [-1, -1, 1] - assert self.tictactoe.minmax(board, "O") == [-1, -1, -1] - - board = [ - ["X", "X", self.tictactoe.empty_indicator], + ) + assert tictactoe.empty_cells(board) == [(0, 2), (1, 1), (2, 0)] + + +@patch("builtins.input") +def test_get_player_number(input_mock: MagicMock, tictactoe: TicTacToe): + """Tests get_player_number. + + Args: + input_mock (MagicMock): Mock of the input function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + input_mock.side_effect = ["y", "n", "", "Y", 1, "N"] + tictactoe.get_player_number() + assert tictactoe.ai_opponent is True + assert input_mock.call_count == 1 + tictactoe.get_player_number() + assert tictactoe.ai_opponent is False + assert input_mock.call_count == 2 + tictactoe.get_player_number() + assert tictactoe.ai_opponent is True + assert input_mock.call_count == 4 + tictactoe.get_player_number() + assert tictactoe.ai_opponent is False + assert input_mock.call_count == 6 + + +@patch("builtins.input") +def test_get_ai_start(input_mock: MagicMock, tictactoe: TicTacToe): + """Tests get_ai_start. + + Args: + input_mock (MagicMock): Mock of the input function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + input_mock.side_effect = ["y", "n", "", "Y", 1, "N"] + tictactoe.get_ai_start() + assert tictactoe.ai_marker == "X" + assert input_mock.call_count == 1 + tictactoe.get_ai_start() + assert tictactoe.ai_marker == "O" + assert input_mock.call_count == 2 + tictactoe.get_ai_start() + assert tictactoe.ai_marker == "X" + assert input_mock.call_count == 4 + tictactoe.get_ai_start() + assert tictactoe.ai_marker == "O" + assert input_mock.call_count == 6 + + +@patch("builtins.input") +def test_get_ai_strength(input_mock: MagicMock, tictactoe: TicTacToe): + """Tests get_ai_start. + + Args: + input_mock (MagicMock): Mock of the input function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + input_mock.side_effect = ["1", "2", "", "3", "X", "4"] + tictactoe.get_ai_strength() + assert tictactoe.ai_function == tictactoe.random_move + assert input_mock.call_count == 1 + tictactoe.get_ai_strength() + assert tictactoe.ai_function == tictactoe.win_move + assert input_mock.call_count == 2 + tictactoe.get_ai_strength() + assert tictactoe.ai_function == tictactoe.block_win_move + assert input_mock.call_count == 4 + tictactoe.get_ai_strength() + assert tictactoe.ai_function == tictactoe.minmax + assert input_mock.call_count == 6 + + +def test_minmax(tictactoe: TicTacToe): + """Tests minmax. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + board = Board([["X", "X", "X"], ["X", "X", "X"], ["X", "X", "X"]]) + assert tictactoe.minmax(board, "X") == Move(-1, -1, EndState.WIN) + assert tictactoe.minmax(board, "O") == Move(-1, -1, EndState.LOSS) + + board = Board( + [ + ["X", "X", tictactoe.empty_indicator], ["O", "X", "O"], ["X", "O", "O"], ] - assert self.tictactoe.minmax(board, "X") == [0, 2, 1] - assert self.tictactoe.minmax(board, "O") == [0, 2, 1] - board = [ + ) + assert tictactoe.minmax(board, "X") == Move(0, 2, EndState.WIN) + assert tictactoe.minmax(board, "O") == Move(0, 2, EndState.WIN) + board = Board( + [ ["O", "O", "X"], - ["X", self.tictactoe.empty_indicator, "O"], - [self.tictactoe.empty_indicator, "O", "X"], + ["X", tictactoe.empty_indicator, "O"], + [tictactoe.empty_indicator, "O", "X"], ] - assert self.tictactoe.minmax(board, "X") == [1, 1, 0] - board = [ + ) + assert tictactoe.minmax(board, "X") == Move(1, 1, EndState.DRAW) + board = Board( + [ ["O", "O", "X"], - ["X", self.tictactoe.empty_indicator, self.tictactoe.empty_indicator], - [self.tictactoe.empty_indicator, "O", "X"], + ["X", tictactoe.empty_indicator, tictactoe.empty_indicator], + [tictactoe.empty_indicator, "O", "X"], ] - assert self.tictactoe.minmax(board, "O") == [1, 1, 1] - board = [ + ) + assert tictactoe.minmax(board, "O") == Move(1, 1, EndState.WIN) + board = Board( + [ [ "O", - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], ] - assert self.tictactoe.minmax(board, "X") == [1, 1, 0] - - @patch("tictactoe.tictactoe_python.TicTacToe.minmax") - @patch("tictactoe.tictactoe_python.TicTacToe.show_board") - def test_ai_turn(self, show_mock: MagicMock, minmax_mock: MagicMock): - """Tests ai_turn. - - Args: - show_mock (MagicMock): Mock out show_board - minmax_mock (MagicMock): Mock out minmax - """ - minmax_mock.side_effect = [[0, 0, 1], [1, 0, 0], [2, 2, 1]] - self.tictactoe.ai_function = minmax_mock - self.tictactoe.create_board() - assert self.tictactoe.board[0][0] == self.tictactoe.empty_indicator - assert self.tictactoe.board[1][0] == self.tictactoe.empty_indicator - assert self.tictactoe.board[2][2] == self.tictactoe.empty_indicator - self.tictactoe.ai_turn("X") - assert show_mock.call_count == 1 - assert minmax_mock.call_count == 1 - assert self.tictactoe.board[0][0] == "X" - self.tictactoe.ai_turn("X") - assert show_mock.call_count == 2 - assert minmax_mock.call_count == 2 - assert self.tictactoe.board[1][0] == "X" - self.tictactoe.ai_turn("O") - assert show_mock.call_count == 3 - assert minmax_mock.call_count == 3 - assert self.tictactoe.board[2][2] == "O" - - @patch("tictactoe.tictactoe_python.TicTacToe.fix_spot") - @patch("tictactoe.tictactoe_python.TicTacToe.get_player_input") - @patch("tictactoe.tictactoe_python.TicTacToe.show_board") - def test_player_turn( - self, show_mock: MagicMock, input_mock: MagicMock, fix_mock: MagicMock - ): - """Tests player_turn. - - Args: - show_mock (MagicMock): Mock put show_board - input_mock (MagicMock): Mock out user input - fix_mock (MagicMock): Mock out making the move - """ - input_mock.side_effect = [False, (1, 1), (3, 3)] - fix_mock.side_effect = [False, True] - self.tictactoe.player_turn("X") - assert show_mock.call_count == 3 - assert input_mock.call_count == 3 - assert fix_mock.call_count == 2 - - def test_random_move(self): - """Tests random_move.""" - random_move = self.tictactoe.random_move( + ) + assert tictactoe.minmax(board, "X") == Move(1, 1, EndState.DRAW) + + +@patch("tictactoe.tictactoe_python.TicTacToe.minmax") +def test_ai_turn(minmax_mock: MagicMock, tictactoe: TicTacToe): + """Tests ai_turn. + + Args: + minmax_mock (MagicMock): Mock of minmax function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + minmax_mock.side_effect = [ + Move(0, 0, EndState.WIN), + Move(1, 0, EndState.DRAW), + Move(2, 2, EndState.WIN), + ] + tictactoe.ai_function = minmax_mock + + assert tictactoe.board[0, 0] == tictactoe.empty_indicator + assert tictactoe.board[1, 0] == tictactoe.empty_indicator + assert tictactoe.board[2, 2] == tictactoe.empty_indicator + tictactoe.ai_turn("X") + assert minmax_mock.call_count == 1 + assert tictactoe.board[0, 0] == "X" + tictactoe.ai_turn("X") + assert minmax_mock.call_count == 2 + assert tictactoe.board[1, 0] == "X" + tictactoe.ai_turn("O") + assert minmax_mock.call_count == 3 + assert tictactoe.board[2, 2] == "O" + + +@patch("tictactoe.tictactoe_python.TicTacToe.fix_spot") +@patch("tictactoe.tictactoe_python.TicTacToe.get_player_input") +def test_player_turn( + input_mock: MagicMock, + fix_mock: MagicMock, + tictactoe: TicTacToe, +): + """Tests player_turn. + + Args: + input_mock (MagicMock): Mock of the input function. + fix_mock (MagicMock): Mock of the fix_spot function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + input_mock.side_effect = [False, (1, 1), (3, 3)] + fix_mock.side_effect = [False, True] + tictactoe.player_turn("X") + assert input_mock.call_count == 3 + assert fix_mock.call_count == 2 + + +def test_random_move(tictactoe: TicTacToe): + """Tests random_move. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + random_move = tictactoe.random_move( + Board( [ [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], [ - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, - self.tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, + tictactoe.empty_indicator, ], - ], - "X", - ) - assert len(random_move) == 3 - assert random_move[0] in range(3) - assert random_move[1] in range(3) - assert random_move[2] == 0 - assert self.tictactoe.random_move( + ] + ), + "X", + ) + assert isinstance(random_move, Move) + assert random_move.row in range(3) + assert random_move.col in range(3) + assert random_move.score == EndState.DRAW + assert tictactoe.random_move( + Board( [ - ["X", "X", self.tictactoe.empty_indicator], + ["X", "X", tictactoe.empty_indicator], ["O", "X", "O"], ["X", "O", "O"], - ], - "X", - ) == [0, 2, 0] - - @patch("tictactoe.tictactoe_python.TicTacToe._get_winning_move") - @patch("tictactoe.tictactoe_python.TicTacToe.random_move") - def test_win_move(self, random_move_mock: MagicMock, winning_move_mock: MagicMock): - """Tests win_move. - - Args: - random_move_mock (MagicMock): Mock out the random move - winning_move_mock (MagicMock): Mock out the actual calculating function - """ - winning_move_mock.side_effect = [[0, 0, 1], None] - random_move_mock.side_effect = [[1, 1, 1]] - assert self.tictactoe.win_move([], "X") == [0, 0, 1] - assert winning_move_mock.call_count == 1 - assert random_move_mock.call_count == 0 - assert self.tictactoe.win_move([], "X") == [1, 1, 1] - assert winning_move_mock.call_count == 2 - assert random_move_mock.call_count == 1 - - def test_get_winning_move(self): - """Tests get_winning_move.""" - # Finds win on row - board = [["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]] - assert self.tictactoe._get_winning_move(board, "X") == [2, 2, 0] - # Finds win on col - board = [["O", "X", "-"], ["-", "O", "-"], ["O", "X", "X"]] - assert self.tictactoe._get_winning_move(board, "O") == [1, 0, 0] - # Finds win on diagonal - board = [["O", "-", "-"], ["-", "O", "-"], ["-", "-", "-"]] - assert self.tictactoe._get_winning_move(board, "O") == [2, 2, 0] - # Finds win on antidiagonal - board = [["-", "-", "-"], ["-", "X", "-"], ["X", "-", "-"]] - assert self.tictactoe._get_winning_move(board, "X") == [0, 2, 0] - # Finds no win - board = [["O", "X", "X"], ["-", "O", "-"], ["O", "X", "-"]] - assert self.tictactoe._get_winning_move(board, "X") is None - - @patch("tictactoe.tictactoe_python.TicTacToe._get_winning_move") - @patch("tictactoe.tictactoe_python.TicTacToe._get_blocking_move") - @patch("tictactoe.tictactoe_python.TicTacToe.random_move") - def test_block_win_move( - self, - random_move_mock: MagicMock, - blocking_move_mock: MagicMock, - winning_move_mock: MagicMock, - ): - """Tests block_win_move. - - Args: - random_move_mock (MagicMock): Mock out the random move - blocking_move_mock (MagicMock): Mock out the move calculating function 1 - winning_move_mock (MagicMock): Mock out the move calculating function 2 - """ - winning_move_mock.side_effect = [[0, 0, 1], None, None] - blocking_move_mock.side_effect = [[2, 2, -1], None] - random_move_mock.side_effect = [[1, 1, 0]] - assert self.tictactoe.block_win_move([], "X") == [0, 0, 1] - assert winning_move_mock.call_count == 1 - assert blocking_move_mock.call_count == 0 - assert random_move_mock.call_count == 0 - assert self.tictactoe.block_win_move([], "X") == [2, 2, -1] - assert winning_move_mock.call_count == 2 - assert blocking_move_mock.call_count == 1 - assert random_move_mock.call_count == 0 - assert self.tictactoe.block_win_move([], "X") == [1, 1, 0] - assert winning_move_mock.call_count == 3 - assert blocking_move_mock.call_count == 2 - assert random_move_mock.call_count == 1 - - def test_get_blocking_move(self): - """Tests get_blocking_move.""" - # Finds block on row - board = [["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]] - assert self.tictactoe._get_blocking_move(board, "O") == [2, 2, 0] - # Finds block on col - board = [["O", "-", "-"], ["-", "-", "-"], ["O", "X", "X"]] - assert self.tictactoe._get_blocking_move(board, "X") == [1, 0, 0] - # Finds block on diagonal - board = [["O", "-", "-"], ["-", "O", "-"], ["-", "-", "-"]] - assert self.tictactoe._get_blocking_move(board, "X") == [2, 2, 0] - # Finds block on antidiagonal - board = [["-", "-", "-"], ["-", "X", "-"], ["X", "-", "-"]] - assert self.tictactoe._get_blocking_move(board, "O") == [0, 2, 0] - # Finds no block - board = [["O", "X", "X"], ["-", "O", "-"], ["O", "X", "-"]] - assert self.tictactoe._get_blocking_move(board, "O") is None + ] + ), + "X", + ) == Move(0, 2, EndState.DRAW) + + +@patch("tictactoe.tictactoe_python.TicTacToe._get_winning_move") +@patch("tictactoe.tictactoe_python.TicTacToe.random_move") +def test_win_move( + random_move_mock: MagicMock, winning_move_mock: MagicMock, tictactoe: TicTacToe +): + """Tests win_move. + + Args: + random_move_mock (MagicMock): Mock of the random_move function. + winning_move_mock (MagicMock): Mock of the _get_winning_move function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + winning_move_mock.side_effect = [Move(0, 0, EndState.WIN), None] + random_move_mock.side_effect = [Move(1, 1, EndState.WIN)] + assert tictactoe.win_move([], "X") == Move(0, 0, EndState.WIN) + assert winning_move_mock.call_count == 1 + assert random_move_mock.call_count == 0 + assert tictactoe.win_move([], "X") == Move(1, 1, EndState.WIN) + assert winning_move_mock.call_count == 2 + assert random_move_mock.call_count == 1 + + +def test_get_winning_move(tictactoe: TicTacToe): + """Tests get_winning_move. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + # Finds win on row + board = Board([["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]]) + assert tictactoe._get_winning_move(board, "X") == Move(2, 2, EndState.DRAW) + # Finds win on col + board = Board([["O", "X", "-"], ["-", "O", "-"], ["O", "X", "X"]]) + assert tictactoe._get_winning_move(board, "O") == Move(1, 0, EndState.DRAW) + # Finds win on diagonal + board = Board([["O", "-", "-"], ["-", "O", "-"], ["-", "-", "-"]]) + assert tictactoe._get_winning_move(board, "O") == Move(2, 2, EndState.DRAW) + # Finds win on antidiagonal + board = Board([["-", "-", "-"], ["-", "X", "-"], ["X", "-", "-"]]) + assert tictactoe._get_winning_move(board, "X") == Move(0, 2, EndState.DRAW) + # Finds no win + board = Board([["O", "X", "X"], ["-", "O", "-"], ["O", "X", "-"]]) + assert tictactoe._get_winning_move(board, "X") is None + + +@patch("tictactoe.tictactoe_python.TicTacToe._get_winning_move") +@patch("tictactoe.tictactoe_python.TicTacToe._get_blocking_move") +@patch("tictactoe.tictactoe_python.TicTacToe.random_move") +def test_block_win_move( + random_move_mock: MagicMock, + blocking_move_mock: MagicMock, + winning_move_mock: MagicMock, + tictactoe: TicTacToe, +): + """Tests block_win_move. + + Args: + random_move_mock (MagicMock): Mock of the random_move function. + blocking_move_mock (MagicMock): Mock of the _get_blocking_move function. + winning_move_mock (MagicMock): Mock of the _get_winning_move function. + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + winning_move_mock.side_effect = [Move(0, 0, EndState.WIN), None, None] + blocking_move_mock.side_effect = [Move(2, 2, EndState.LOSS), None] + random_move_mock.side_effect = [Move(1, 1, EndState.DRAW)] + assert tictactoe.block_win_move([], "X") == Move(0, 0, EndState.WIN) + assert winning_move_mock.call_count == 1 + assert blocking_move_mock.call_count == 0 + assert random_move_mock.call_count == 0 + assert tictactoe.block_win_move([], "X") == Move(2, 2, EndState.LOSS) + assert winning_move_mock.call_count == 2 + assert blocking_move_mock.call_count == 1 + assert random_move_mock.call_count == 0 + assert tictactoe.block_win_move([], "X") == Move(1, 1, EndState.DRAW) + assert winning_move_mock.call_count == 3 + assert blocking_move_mock.call_count == 2 + assert random_move_mock.call_count == 1 + + +def test_get_blocking_move(tictactoe: TicTacToe): + """Tests get_blocking_move. + + Args: + tictactoe (TicTacToe): TicTacToe instance to test against. + """ + # Finds block on row + board = Board([["O", "X", "O"], ["X", "O", "-"], ["X", "X", "-"]]) + assert tictactoe._get_blocking_move(board, "O") == Move(2, 2, EndState.DRAW) + # Finds block on col + board = Board([["O", "-", "-"], ["-", "-", "-"], ["O", "X", "X"]]) + assert tictactoe._get_blocking_move(board, "X") == Move(1, 0, EndState.DRAW) + # Finds block on diagonal + board = Board([["O", "-", "-"], ["-", "O", "-"], ["-", "-", "-"]]) + assert tictactoe._get_blocking_move(board, "X") == Move(2, 2, EndState.DRAW) + # Finds block on antidiagonal + board = Board([["-", "-", "-"], ["-", "X", "-"], ["X", "-", "-"]]) + assert tictactoe._get_blocking_move(board, "O") == Move(0, 2, EndState.DRAW) + # Finds no block + board = Board([["O", "X", "X"], ["-", "O", "-"], ["O", "X", "-"]]) + assert tictactoe._get_blocking_move(board, "O") is None diff --git a/tictactoe_python/tictactoe/tictactoe_python.py b/tictactoe_python/tictactoe/tictactoe_python.py index d9f05a1..c43b368 100755 --- a/tictactoe_python/tictactoe/tictactoe_python.py +++ b/tictactoe_python/tictactoe/tictactoe_python.py @@ -4,12 +4,183 @@ import itertools import random import sys -from collections.abc import Callable +from collections.abc import Callable, Iterable, Iterator, Sized +from dataclasses import dataclass +from enum import Enum from time import sleep +from typing import Self -Board = list[list[str]] +from typing_extensions import override -AIFunction = Callable[[Board, str], list[int]] + +class Board(Iterable[list[str]], Sized): + """Class that represents a TicTacToe board.""" + + def __init__( + self, + board: list[list[str]] | None = None, + empty_indicator: str = "-", + size: int = 3, + ) -> None: + """Initialize a board as a list of lists. + + Args: + board (list[list[str]] | None): 2D list that contains the ticatactoe board. (Default value = None) + empty_indicator (str): String to indicate an unoccupied spot on the board. + (Default value = '-') + size (int): Size of the board. (Default value = 3) + """ + self._empty_indicator = empty_indicator + self._size = size + self._board: list[list[str]] = board or [ + [self._empty_indicator] * self._size for _ in range(self._size) + ] + + @override + def __str__(self) -> str: + """Return the board as a string. + + Returns: + str: Nice string representation of the board + """ + line_separator = "---------------" + lines = [line_separator] + for row in self._board: + row_str = "".join(f"| {item} |" for item in row) + lines.append(row_str) + lines.append(line_separator) + return "\n".join(lines) + + @override + def __repr__(self) -> str: + """Return the board as a string. + + Returns: + str: Raw representation of the board data. + """ + return repr(self._board) + + def __getitem__(self, positions: tuple[int, int]) -> str: + """Get element from board. + + Args: + positions (tuple[int, int]): Tuple of row and column index to get from the board. + + Returns: + str: Item at the given position on the board + """ + return self._board[positions[0]][positions[1]] + + def __setitem__(self, positions: tuple[int, int], value: str) -> None: + """Set element on board. + + Args: + positions (tuple[int, int]): Tuple of row and column index to set on the board. + value (str): Value to set. + """ + self._board[positions[0]][positions[1]] = value + + @override + def __len__(self) -> int: + """Return the size of the board. + + Returns: + int: Size of the board. + """ + return self._size + + @override + def __iter__(self) -> Iterator[list[str]]: + """Return an iterator over the board. + + Returns: + Iterator[list[str]]: Iterator over the board. + """ + return iter(self._board) + + +class OrderedEnum(Enum): + """Enum with order relations propagated from the values.""" + + def __ge__(self, other: Self) -> bool: + """Greater or equal comparison by value. + + Args: + other (Self): Other value to compare against. + + Returns: + bool: True if the value is greater or equal to the other value. + """ + if self.__class__ is other.__class__: + return self.value >= other.value + return NotImplemented + + def __gt__(self, other: Self) -> bool: + """Greater than comparison by value. + + Args: + other (Self): Other value to compare against. + + Returns: + bool: True if the value is greater than the other value. + """ + if self.__class__ is other.__class__: + return self.value > other.value + return NotImplemented + + def __le__(self, other: Self) -> bool: + """Less or equal comparison by value. + + Args: + other (Self): Other value to compare against. + + Returns: + bool: True if the value is less or equal to the other value. + """ + if self.__class__ is other.__class__: + return self.value <= other.value + return NotImplemented + + def __lt__(self, other: Self) -> bool: + """Less than equal comparison by value. + + Args: + other (Self): Other value to compare against. + + Returns: + bool: True if the value is less then the other value. + """ + if self.__class__ is other.__class__: + return self.value < other.value + return NotImplemented + + +class EndState(OrderedEnum): + """Enum for possible end states of a game.""" + + LOSS = -1 + DRAW = 0 + WIN = 1 + + def __neg__(self) -> "EndState": + """Get the opposite outcome of a game. + + Returns: + 'EndState': Opposite outcome of the game. + """ + return EndState(-self.value) + + +@dataclass +class Move: + """Dataclass for an AI function move with position and final game outcome.""" + + row: int + col: int + score: EndState + + +AIFunction = Callable[[Board, str], Move] class TicTacToe: @@ -27,19 +198,12 @@ class TicTacToe: def __init__(self) -> None: """Initialize a game of TicTacToe.""" - self.board: Board = [] self.empty_indicator: str = "-" + self.board = Board(empty_indicator=self.empty_indicator) self.ai_opponent: bool = False self.ai_marker: str = "X" self.ai_function: AIFunction = self.minmax - def create_board(self) -> None: - """Initializes the board as a list of lists containing only '-'.""" - self.board = [] - for _ in range(3): - row = [self.empty_indicator for _ in range(3)] - self.board.append(row) - def fix_spot(self, row: int, col: int, player: str) -> bool: """Tries to set a given position on the board to the value of the given player. @@ -61,13 +225,13 @@ def fix_spot(self, row: int, col: int, player: str) -> bool: " They have to be between 1 and 3 inclusive. Try again!" ) return False - if self.board[row][col] != self.empty_indicator: + if self.board[row, col] != self.empty_indicator: print( f"The position ({row+1}, {col+1}) has already been taken by a player!" " Please do your move on an empty position." ) return False - self.board[row][col] = player + self.board[row, col] = player return True def _is_row_win(self, player: str, board: Board) -> bool: @@ -82,7 +246,7 @@ def _is_row_win(self, player: str, board: Board) -> bool: """ board_length = len(board) for i in range(board_length): - if win := all(board[i][j] == player for j in range(board_length)): + if win := all(board[i, j] == player for j in range(board_length)): return win return False @@ -98,7 +262,7 @@ def _is_column_win(self, player: str, board: Board) -> bool: """ board_length = len(board) for i in range(board_length): - if win := all(board[j][i] == player for j in range(board_length)): + if win := all(board[j, i] == player for j in range(board_length)): return win return False @@ -113,11 +277,11 @@ def _is_diagonal_win(self, player: str, board: Board) -> bool: bool: Whether the player won via a diagonal """ board_length = len(board) - if win := all(board[i][i] == player for i in range(board_length)): + if win := all(board[i, i] == player for i in range(board_length)): return win if win := all( - board[i][board_length - 1 - i] == player for i in range(board_length) + board[i, board_length - 1 - i] == player for i in range(board_length) ): return win return False @@ -163,15 +327,6 @@ def swap_player_turn(self, player: str) -> str: """ return "X" if player == "O" else "O" - def show_board(self) -> None: - """Prints out the current board.""" - line_separator = "---------------" - print(line_separator) - for row in self.board: - for item in row: - print(f"| {item} |", end="") - print(f"\n{line_separator}") - def get_player_input(self) -> tuple[int, int] | None: """Asks the player for input and validates it. @@ -206,10 +361,10 @@ def ai_turn(self, player: str) -> None: player (str): Which side the AI is playing on """ print(f"AI turn as {player}.") - self.show_board() + print(self.board) print(flush=True) - row, col, _ = self.ai_function(self.board, player) - self.board[row][col] = player + move = self.ai_function(self.board, player) + self.board[move.row, move.col] = player sleep(1) def empty_cells(self, board: Board) -> list[tuple[int, int]]: @@ -223,12 +378,12 @@ def empty_cells(self, board: Board) -> list[tuple[int, int]]: """ return [ (row, col) - for row, col in itertools.product(range(len(board)), range(len(board[0]))) - if board[row][col] == self.empty_indicator + for row, col in itertools.product(range(len(board)), range(len(board))) + if board[row, col] == self.empty_indicator ] # Move could probably become a dataclass - def minmax(self, board: Board, player: str) -> list[int]: + def minmax(self, board: Board, player: str) -> Move: """Takes a board state and return the optimal move for the given player. Args: @@ -236,30 +391,30 @@ def minmax(self, board: Board, player: str) -> list[int]: player (str): The player whose move it currently is Returns: - list[int]: [row, col, value] of best move + Move: [row, col, value] of best move """ - best_move = [-1, -1, -1] + best_move = Move(-1, -1, EndState.LOSS) if self.is_player_win(player, board): - best_move[2] = 1 + best_move.score = EndState.WIN return best_move if self.is_player_win(self.swap_player_turn(player), board): - best_move[2] = -1 + best_move.score = EndState.LOSS return best_move empty_cells = self.empty_cells(board) if not empty_cells: - best_move[2] = 0 + best_move.score = EndState.DRAW return best_move if len(empty_cells) == len(board) ** 2: - return [random.randint(0, 2), random.randint(0, 2), 0] + return Move(random.randint(0, 2), random.randint(0, 2), EndState.DRAW) for row, col in empty_cells: - board[row][col] = player - _, _, value = self.minmax(board, self.swap_player_turn(player)) - if -value >= best_move[2]: - best_move = [row, col, -value] - board[row][col] = self.empty_indicator + board[row, col] = player + value = self.minmax(board, self.swap_player_turn(player)).score + if -value >= best_move.score: + best_move = Move(row, col, -value) + board[row, col] = self.empty_indicator return best_move - def random_move(self, board: Board, _: str) -> list[int]: + def random_move(self, board: Board, _: str) -> Move: """Takes a board state and returns the coordinates of a valid random move. Args: @@ -269,13 +424,13 @@ def random_move(self, board: Board, _: str) -> list[int]: AI move functions Returns: - list[int]: [row, col, value] of a random valid move + Move: [row, col, value] of a random valid move """ empty_cells = self.empty_cells(board) cell = random.choice(empty_cells) - return [cell[0], cell[1], 0] + return Move(cell[0], cell[1], EndState.DRAW) - def win_move(self, board: Board, player: str) -> list[int]: + def win_move(self, board: Board, player: str) -> Move: """Take a board state and return either a winning or random move. Args: @@ -283,7 +438,7 @@ def win_move(self, board: Board, player: str) -> list[int]: player (str): The player whose move it currently is. Returns: - list[int]: [row, col, value] of best move + Move: [row, col, value] of best move """ winning_move = self._get_winning_move(board, player) if winning_move is None: @@ -292,7 +447,7 @@ def win_move(self, board: Board, player: str) -> list[int]: def _get_winning_move( # noqa: C901 self, board: Board, player: str - ) -> list[int] | None: + ) -> Move | None: """Takes a board state and returns the coordinates of a winning move or None. Args: @@ -300,7 +455,7 @@ def _get_winning_move( # noqa: C901 player (str): The player whose move it currently is. Returns: - list[int] | None: [row, col, value] of a winning move + Move | None: [row, col, value] of a winning move or None if there is none. """ win_conditions: dict[str, set[tuple[int, int]]] = { @@ -343,10 +498,10 @@ def _get_winning_move( # noqa: C901 for moves in win_conditions.values(): if len(moves) == 1: winning_move = moves.pop() - return [winning_move[0], winning_move[1], 0] + return Move(winning_move[0], winning_move[1], EndState.DRAW) return None - def block_win_move(self, board: Board, player: str) -> list[int]: + def block_win_move(self, board: Board, player: str) -> Move: """Take a board state and return either a winning, blocking or random move. Args: @@ -354,7 +509,7 @@ def block_win_move(self, board: Board, player: str) -> list[int]: player (str): The player whose move it currently is. Returns: - list[int]: [row, col, value] of best move + Move: [row, col, value] of best move """ winning_move = self._get_winning_move(board, player) if winning_move is not None: @@ -364,7 +519,7 @@ def block_win_move(self, board: Board, player: str) -> list[int]: return blocking_move return self.random_move(board, player) - def _get_blocking_move(self, board: Board, player: str) -> list[int] | None: + def _get_blocking_move(self, board: Board, player: str) -> Move | None: """Takes a board state and returns the coordinates of a blocking move or None. Args: @@ -372,7 +527,7 @@ def _get_blocking_move(self, board: Board, player: str) -> list[int] | None: player (str): The player whose move it currently is. Returns: - list[int] | None: [row, col, value] of a blocking move + Move | None: [row, col, value] of a blocking move or None if there is none. """ return self._get_winning_move(board, self.swap_player_turn(player)) @@ -387,7 +542,7 @@ def player_turn(self, player: str) -> None: while not valid_move: print(f"Player {player} turn") - self.show_board() + print(self.board) player_input = self.get_player_input() if not player_input: @@ -457,8 +612,6 @@ def get_ai_strength(self) -> None: def start(self) -> None: """Starts the game and contains the main game loop.""" - self.create_board() - self.get_player_number() if self.ai_opponent: self.get_ai_strength() @@ -486,7 +639,7 @@ def start(self) -> None: # showing the final view of board print() - self.show_board() + print(self.board) # starting the game