From 8a14b0033a2c35526154c16d0a2ca8da2fe48de7 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Tue, 10 Aug 2021 13:41:46 +0300 Subject: [PATCH] Initial release: added Game class and tests for it --- .gitignore | 7 ++ LICENCE | 19 +++++ README.md | 15 ++++ pyproject.toml | 3 + setup.py | 26 +++++++ src/tic_tac_toe/__init__.py | 3 + src/tic_tac_toe/game.py | 92 ++++++++++++++++++++++ tests/__init__.py | 0 tests/test_game.py | 151 ++++++++++++++++++++++++++++++++++++ 9 files changed, 316 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/tic_tac_toe/__init__.py create mode 100644 src/tic_tac_toe/game.py create mode 100644 tests/__init__.py create mode 100644 tests/test_game.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1383710 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +dist/ + +.venv/ +.vscode/ +__pycache__/ +.coverage +.pytest_cache/ \ No newline at end of file diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..96f1555 --- /dev/null +++ b/LICENCE @@ -0,0 +1,19 @@ +Copyright (c) 2018 The Python Packaging Authority + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e4a0a5 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Tic Tac Toe + +A simple tic tac toe game implementation + +## Usage + +tic-tac-toe module exports main game class `Game` and `Pl` and `Tr` enums to simplify typing. + +### `Game` class + +TODO: Add Game class description + +### `Pl` and `Tr` Enums + +`Pl` has two members: `X` and `O` meaning X and O players. `Tr` enum has all members of `Pl` plus `E` entry meaning empty cell. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07de284 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d142369 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +import setuptools + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setuptools.setup( + name="tic-tac-toe-dm1sh", + version="0.0.1", + author="dm1sh", + author_email="me@dmitriy.icu", + description="A simple tic tac toe game implementation", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/dm1sh/tic-tac-toe", + project_urls={ + "Bug Tracker": "https://github.com/dm1sh/tic-tac-toe/issues", + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + package_dir={"": "src"}, + packages=setuptools.find_packages(where="src"), + python_requires=">=3.6", +) diff --git a/src/tic_tac_toe/__init__.py b/src/tic_tac_toe/__init__.py new file mode 100644 index 0000000..bc33ccf --- /dev/null +++ b/src/tic_tac_toe/__init__.py @@ -0,0 +1,3 @@ +from .game import Game, Tr, Pl + +__all__ = [Game, Tr, Pl] diff --git a/src/tic_tac_toe/game.py b/src/tic_tac_toe/game.py new file mode 100644 index 0000000..3a4b4e2 --- /dev/null +++ b/src/tic_tac_toe/game.py @@ -0,0 +1,92 @@ +from typing import List, Tuple +from enum import Enum + + +class Pl(Enum): + X = 3 + O = 2 + + +class Tr(Enum): + E = 0 + X = Pl.X + O = Pl.O + + +class Game: + """ + Board indexes preview: + + [[0, 1, 2], + + [3, 4, 5], + + [6, 7, 8]] + """ + _board: List[Tr] + + def __init__(self): + self._board = [Tr.E] * 9 + + def get_board(self) -> List[Tr]: + """ + Returns copy of game board. Use it to display board in UI. + """ + return self._board.copy() + + def check_move(self, pos: int) -> bool: + """ + Checks if board cell empty + """ + return self._board[pos] == Tr.E + + def check_filled(self) -> bool: + """ + Checks if game board is filled + """ + return self._board.count(Tr.E) == 0 + + def check_win(self, pos: int) -> bool: + """ + Checks if this move will make player a winner. + """ + b = self._board + + row_n = pos - pos % 3 + row = b[row_n] == b[row_n + 1] == b[row_n + 2] != Tr.E + + col_n = pos % 3 + col = b[col_n] == b[col_n + 3] == b[col_n + 6] != Tr.E + + diag = False + alt_diag = False + + if (pos % 4 == 0): + diag = b[0] == b[4] == b[8] != Tr.E + if (pos in (2, 4, 6)): + alt_diag = b[2] == b[4] == b[6] != Tr.E + + return row or col or diag or alt_diag + + def insert(self, pos: int, who: Tr) -> None: + """ + Sets game board's cell to specified value. Better use `move` method when possible + """ + self._board[pos] = who + + def move(self, pos: int, who: Pl) -> bool: + """ + Sets game board cell to specified value when possible. It also returns true if player has won. + """ + if (self.check_move(pos)): + self.insert(pos, who) + return self.check_win(pos) + else: + return False + + def get_free(self) -> Tuple[int]: + """ + Returns + """ + + return tuple(filter(lambda i: self._board[i] == Tr.E, range(9))) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_game.py b/tests/test_game.py new file mode 100644 index 0000000..35fbde5 --- /dev/null +++ b/tests/test_game.py @@ -0,0 +1,151 @@ +from src.tic_tac_toe import Game, Tr + + +def test_init(): + g = Game() + + assert len(g._board) == 9 + + for i in range(9): + assert g._board[i] == Tr.E + + +def setup_insert_1X() -> Game: + g = Game() + g.insert(1, Tr.X) + + return g + + +def setup_insert_13X5O() -> Game: + g = Game() + g.insert(1, Tr.X) + g.insert(5, Tr.O) + g.insert(3, Tr.X) + + return g + + +def test_insert_1X(): + g = setup_insert_1X() + + b = g._board + assert b.count(Tr.X) == 1 + assert b.count(Tr.O) == 0 + assert b.count(Tr.E) == 8 + + +def test_insert_1X5O(): + g = setup_insert_13X5O() + + b = g._board + assert b.count(Tr.X) == 2 + assert b.count(Tr.O) == 1 + assert b.count(Tr.E) == 6 + + assert b[1] == Tr.X + assert b[3] == Tr.X + assert b[5] == Tr.O + + +def test_get_board(): + g1 = setup_insert_1X() + g2 = setup_insert_13X5O() + + for g in (g1, g2): + b = g._board + test_b = g.get_board() + + for el in Tr: + assert b.count(el) == test_b.count(el) + + +def test_move(): + g = Game() + + res = g.move(0, Tr.X) + assert res == False + res_board = g.get_board() + assert res_board[0] == Tr.X + assert res_board.count(Tr.X) == 1 + assert res_board.count(Tr.O) == 0 + assert res_board.count(Tr.E) == 8 + + res = g.move(3, Tr.O) + assert res == False + res_board = g.get_board() + assert res_board[3] == Tr.O + assert res_board.count(Tr.X) == 1 + assert res_board.count(Tr.O) == 1 + assert res_board.count(Tr.E) == 7 + + res = g.move(1, Tr.X) + assert res == False + res_board = g.get_board() + assert res_board[1] == Tr.X + assert res_board.count(Tr.X) == 2 + assert res_board.count(Tr.O) == 1 + assert res_board.count(Tr.E) == 6 + + res = g.move(2, Tr.X) + assert res == True + res_board = g.get_board() + assert res_board[2] == Tr.X + assert res_board.count(Tr.X) == 3 + assert res_board.count(Tr.O) == 1 + assert res_board.count(Tr.E) == 5 + + res = g.move(2, Tr.X) + assert res == False + + +def test_get_free(): + g1 = setup_insert_1X() + g2 = setup_insert_13X5O() + + for g in (g1, g2): + b = g._board + free = g.get_free() + + assert len(free) == b.count(Tr.E) + + for i in free: + assert b[i] == Tr.E + + +def test_check_move(): + g = setup_insert_13X5O() + + assert g.check_move(3) == False + assert g.check_move(0) == True + + +def test_check_filled(): + g = Game() + + assert g.check_filled() == False + + for i in range(8): + g.insert(i, (Tr.X, Tr.O)[i % 2]) + assert g.check_filled() == False + + g.insert(8, Tr.X) + assert g.check_filled() == True + + +def test_check_win(): + for steps in ((0, 1, 2), (0, 3, 6), (0, 4, 8), (2, 4, 6)): + g = Game() + + for i in range(9): + assert g.check_win(i) == False + + g.insert(5, Tr.O) + + for i in range(9): + assert g.check_win(i) == False + + for i in steps: + g.insert(i, Tr.X) + + assert g.check_win(i) == True