Skip to content

Commit d923a82

Browse files
Add type hints and more tests (#4)
1 parent 9f5b30d commit d923a82

5 files changed

Lines changed: 82 additions & 21 deletions

File tree

.github/workflows/code-cov.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CodeCov
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
run:
7+
runs-on: windows-latest
8+
env:
9+
OS: windows-latest
10+
PYTHON: '3.10'
11+
steps:
12+
- uses: actions/checkout@v3
13+
with:
14+
fetch-depth: ‘2’
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v3
18+
with:
19+
python-version: "3.10"
20+
- name: Generate Report
21+
run: |
22+
pip install pytest pytest-cov numpy
23+
pytest test.py --cov=tictactoe --cov-report=xml
24+
coverage report --show-missing
25+
- name: Upload Coverage to Codecov
26+
uses: codecov/codecov-action@v2

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# python-tictactoe
2-
[![PyPI version](https://badge.fury.io/py/python-tictactoe.svg)](https://badge.fury.io/py/python-tictactoe) [![Tests](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/tests.yml/badge.svg)](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/tests.yml) [![Build](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/build.yml/badge.svg)](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/build.yml)
2+
[![PyPI version](https://badge.fury.io/py/python-tictactoe.svg)](https://badge.fury.io/py/python-tictactoe) [![Tests](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/tests.yml/badge.svg)](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/tests.yml) [![Build](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/build.yml/badge.svg)](https://github.com/AttackingOrDefending/python-tictactoe/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/AttackingOrDefending/python-tictactoe/branch/main/graph/badge.svg?token=7N5LHRA3OC)](https://codecov.io/gh/AttackingOrDefending/python-tictactoe)
33

44
A tic-tac-toe library that supports different sizes and dimensions and how many marks in a row to win.
55

@@ -12,6 +12,8 @@ Download and install the latest release:
1212

1313
## Features
1414

15+
* Includes mypy typings.
16+
1517
* Different board sizes
1618
```python
1719
from tictactoe import Board

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = python-tictactoe
3-
version = 0.0.2
3+
version = 0.0.3
44
author = AttackingOrDefending
55
description = A tic-tac-toe library that supports different sizes and dimensions and how many marks in a row to win
66
long_description = file: README.md

test.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from tictactoe import Board
1+
from tictactoe import Board, Move
2+
import numpy
23

34

45
def draw_board():
@@ -60,5 +61,34 @@ def test_result():
6061
assert unfinished_board().result() is None
6162

6263

64+
def test_copy():
65+
board = unfinished_board()
66+
assert board.possible_moves().tolist() == board.copy().possible_moves().tolist()
67+
68+
69+
def test_inbound_outofbounds():
70+
board = unfinished_board()
71+
assert board.in_bounds(numpy.array([2, 2]))
72+
assert board.out_of_bounds(numpy.array([3, 2]))
73+
74+
75+
def test_move():
76+
assert Move((2, 2)).str_move == '2-2'
77+
assert Move(str_move='1-2').coordinate_move == (1, 2)
78+
79+
80+
def test_illegal_move():
81+
board = unfinished_board()
82+
try:
83+
board.push((0, 0))
84+
assert False
85+
except ValueError:
86+
assert True
87+
88+
6389
if __name__ == "__main__":
6490
test_result()
91+
test_copy()
92+
test_inbound_outofbounds()
93+
test_move()
94+
test_illegal_move()

tictactoe/__init__.py

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
1+
from __future__ import annotations
12
import itertools
23
import numpy
4+
from typing import List, Tuple, Iterable, Optional
35

46
X = 1
57
O = 2
68

79

810
class Board:
9-
def __init__(self, dimensions=(3, 3), x_in_a_row=3):
11+
def __init__(self, dimensions: Tuple[int, ...] = (3, 3), x_in_a_row: int = 3) -> None:
1012
self.dimensions = dimensions
1113
self.x_in_a_row = x_in_a_row
1214
self.board = self.create_board()
1315
self._directions = self.find_directions()
1416
self.move_count = 0
15-
self.moves = []
16-
self.x = []
17-
self.o = []
17+
self.moves: List[Move] = []
18+
self.x: List[Move] = []
19+
self.o: List[Move] = []
1820
self.turn = X
1921

20-
def create_board(self):
22+
def create_board(self) -> numpy.ndarray:
2123
return numpy.zeros(self.dimensions)
2224

23-
def copy(self):
25+
def copy(self) -> Board:
2426
board = Board(self.dimensions, self.x_in_a_row)
2527
board.turn = self.turn
2628
board.board = self.board.copy()
2729
return board
2830

29-
def get_mark_at_position(self, position):
31+
def get_mark_at_position(self, position: Iterable[int]) -> int:
3032
position = tuple(position)
3133
return self.board[position]
3234

33-
def set_mark(self, coordinates, player):
35+
def set_mark(self, coordinates: Tuple[int, ...], player: int) -> None:
3436
self.board[coordinates] = player
3537
if player == X:
3638
self.x.append(Move(coordinates))
@@ -40,17 +42,17 @@ def set_mark(self, coordinates, player):
4042
def is_empty(self, position):
4143
return self.get_mark_at_position(position) == 0
4244

43-
def push(self, coordinates):
45+
def push(self, coordinates: Iterable[int]) -> None:
4446
coordinates = tuple(coordinates)
4547
if not self.is_empty(coordinates):
46-
raise Exception("Position is not empty.")
48+
raise ValueError("Position is not empty.")
4749
move = Move(coordinates)
4850
self.set_mark(coordinates, self.turn)
4951
self.turn = X if self.turn == O else O
5052
self.moves.append(move)
5153
self.move_count += 1
5254

53-
def find_directions(self):
55+
def find_directions(self) -> List[Tuple[int, ...]]:
5456
directions = list(itertools.product([1, 0, -1], repeat=len(self.dimensions)))
5557
correct_directions = []
5658
for direction in directions:
@@ -62,16 +64,16 @@ def find_directions(self):
6264
break
6365
return correct_directions
6466

65-
def possible_moves(self):
67+
def possible_moves(self) -> numpy.ndarray:
6668
return numpy.argwhere(self.board == 0)
6769

68-
def out_of_bounds(self, pos):
70+
def out_of_bounds(self, pos: numpy.ndarray) -> numpy.bool_:
6971
return (pos < 0).any() or (pos >= self.dimensions).any()
7072

71-
def in_bounds(self, pos):
73+
def in_bounds(self, pos: numpy.ndarray) -> bool:
7274
return not self.out_of_bounds(pos)
7375

74-
def has_won(self, player):
76+
def has_won(self, player: int) -> bool:
7577
positions = numpy.argwhere(self.board == player)
7678
for position in positions:
7779
for direction in self._directions:
@@ -83,21 +85,22 @@ def has_won(self, player):
8385
return True
8486
return False
8587

86-
def result(self):
88+
def result(self) -> Optional[int]:
8789
if self.has_won(X):
8890
return X
8991
elif self.has_won(O):
9092
return O
9193
elif self.board.all():
9294
return 0
95+
return None
9396

9497

9598
class Move:
96-
def __init__(self, coordinate_move=None, str_move=None):
99+
def __init__(self, coordinate_move: Optional[Tuple[int, ...]] = None, str_move: Optional[str] = None) -> None:
97100
assert coordinate_move or str_move
98101
self.coordinate_move = coordinate_move
99102
self.str_move = str_move
100103
if self.coordinate_move:
101104
self.str_move = "-".join(map(str, tuple(self.coordinate_move)))
102-
else:
105+
elif self.str_move:
103106
self.coordinate_move = tuple(map(int, self.str_move.split("-")))

0 commit comments

Comments
 (0)