├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── checkers ├── __init__.py ├── board.py ├── board_initializer.py ├── board_searcher.py ├── game.py └── piece.py ├── contributing.md ├── readme.md ├── setup.py └── test ├── __init__.py ├── test_game_over.py ├── test_possible_moves.py └── test_winner.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | *.pyc 3 | build/ 4 | dist/ 5 | *.egg-info/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.4' 4 | - '3.5' 5 | - '3.6' 6 | script: 7 | - python -m unittest discover 8 | deploy: 9 | provider: pypi 10 | skip_existing: true 11 | user: janhartigan 12 | password: 13 | secure: UZw0Sm+zqcxNda/VLQPhBLw8YFRqv9VVSLttM/bxRKBnMTu+X56gUTBeN8HfLqbztmMgxQL3M7bRP7voLL10fEP0MnZR0HZGAUPFH0y20GHacxaMP/3dntrSXvhTprRk5Sf70u/wjxTJ1TaB+sNzN75GMxrCljcca8JquRCLSsXH0anWyiGXJQ9oNI0h1zdL4/ujvJmGJI9k9uIhxbqSXfeEsGxz76HWL619jK3a2e7T/trtx7N3721sXKyCB9BlgrpKbyi4kZi1bQvGgnwWsrbKB2fAWPd92Y8ENGu6NR9B/qfeeBcRP77ArP6uqxLT68mUKzWzCEDXjN/wDtf3NgJ4FOB57UOB4QFH7phmPtJM3bq5aIFH+islONgDtS9MniSlmpcdTe6MN4CYLJFYiPQ18fqBtFFSKkbVyhNnYKqw3BUlT6sJd/aKzLG2rQWH3G6Q6T3PKIWlP17pQueBbxcX5YIjByNbLlZVzjSjVrmsEXVwVviOfDzs8xRzWzF/2bXFsdyeQfTnVW8ZpBlUlwUVw8CBHK31pfFgvxZfAkfHm13TPSOWCxLgfBWp1kOTnihqKnwszQFkiOYw0yzj1rEtMfb4NLeEpKgRCPN6xfN3+xUj19155rPF8fExTR2ZIwC3IEQvI1RAXRq4vEp25kxZXEuE6bR1rRWdtIa0N4Y= 14 | on: 15 | tags: true 16 | notifications: 17 | email: 18 | on_success: never -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 ImparaAI https://impara.ai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include readme.md -------------------------------------------------------------------------------- /checkers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImparaAI/checkers/4c41ce84b7b2693846c75110d23cb185580bc805/checkers/__init__.py -------------------------------------------------------------------------------- /checkers/board.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from functools import reduce 3 | from .board_searcher import BoardSearcher 4 | from .board_initializer import BoardInitializer 5 | 6 | class Board: 7 | 8 | def __init__(self): 9 | self.player_turn = 1 10 | self.width = 4 11 | self.height = 8 12 | self.position_count = self.width * self.height 13 | self.rows_per_user_with_pieces = 3 14 | self.position_layout = {} 15 | self.piece_requiring_further_capture_moves = None 16 | self.previous_move_was_capture = False 17 | self.searcher = BoardSearcher() 18 | BoardInitializer(self).initialize() 19 | 20 | def count_movable_player_pieces(self, player_number = 1): 21 | return reduce((lambda count, piece: count + (1 if piece.is_movable() else 0)), self.searcher.get_pieces_by_player(player_number), 0) 22 | 23 | def get_possible_moves(self): 24 | capture_moves = self.get_possible_capture_moves() 25 | 26 | return capture_moves if capture_moves else self.get_possible_positional_moves() 27 | 28 | def get_possible_capture_moves(self): 29 | return reduce((lambda moves, piece: moves + piece.get_possible_capture_moves()), self.searcher.get_pieces_in_play(), []) 30 | 31 | def get_possible_positional_moves(self): 32 | return reduce((lambda moves, piece: moves + piece.get_possible_positional_moves()), self.searcher.get_pieces_in_play(), []) 33 | 34 | def position_is_open(self, position): 35 | return not self.searcher.get_piece_by_position(position) 36 | 37 | def create_new_board_from_move(self, move): 38 | new_board = deepcopy(self) 39 | 40 | if move in self.get_possible_capture_moves(): 41 | new_board.perform_capture_move(move) 42 | else: 43 | new_board.perform_positional_move(move) 44 | 45 | return new_board 46 | 47 | def perform_capture_move(self, move): 48 | self.previous_move_was_capture = True 49 | piece = self.searcher.get_piece_by_position(move[0]) 50 | originally_was_king = piece.king 51 | enemy_piece = piece.capture_move_enemies[move[1]] 52 | enemy_piece.capture() 53 | self.move_piece(move) 54 | further_capture_moves_for_piece = [capture_move for capture_move in self.get_possible_capture_moves() if move[1] == capture_move[0]] 55 | 56 | if further_capture_moves_for_piece and (originally_was_king == piece.king): 57 | self.piece_requiring_further_capture_moves = self.searcher.get_piece_by_position(move[1]) 58 | else: 59 | self.piece_requiring_further_capture_moves = None 60 | self.switch_turn() 61 | 62 | def perform_positional_move(self, move): 63 | self.previous_move_was_capture = False 64 | self.move_piece(move) 65 | self.switch_turn() 66 | 67 | def switch_turn(self): 68 | self.player_turn = 1 if self.player_turn == 2 else 2 69 | 70 | def move_piece(self, move): 71 | self.searcher.get_piece_by_position(move[0]).move(move[1]) 72 | self.pieces = sorted(self.pieces, key = lambda piece: piece.position if piece.position else 0) 73 | 74 | def is_valid_row_and_column(self, row, column): 75 | if row < 0 or row >= self.height: 76 | return False 77 | 78 | if column < 0 or column >= self.width: 79 | return False 80 | 81 | return True 82 | 83 | def __setattr__(self, name, value): 84 | super(Board, self).__setattr__(name, value) 85 | 86 | if name == 'pieces': 87 | [piece.reset_for_new_board() for piece in self.pieces] 88 | 89 | self.searcher.build(self) -------------------------------------------------------------------------------- /checkers/board_initializer.py: -------------------------------------------------------------------------------- 1 | from .piece import Piece 2 | 3 | class BoardInitializer: 4 | 5 | def __init__(self, board): 6 | self.board = board 7 | 8 | def initialize(self): 9 | self.build_position_layout() 10 | self.set_starting_pieces() 11 | 12 | def build_position_layout(self): 13 | self.board.position_layout = {} 14 | position = 1 15 | 16 | for row in range(self.board.height): 17 | self.board.position_layout[row] = {} 18 | 19 | for column in range(self.board.width): 20 | self.board.position_layout[row][column] = position 21 | position += 1 22 | 23 | def set_starting_pieces(self): 24 | pieces = [] 25 | starting_piece_count = self.board.width * self.board.rows_per_user_with_pieces 26 | player_starting_positions = { 27 | 1: list(range(1, starting_piece_count + 1)), 28 | 2: list(range(self.board.position_count - starting_piece_count + 1, self.board.position_count + 1)) 29 | } 30 | 31 | for key, row in self.board.position_layout.items(): 32 | for key, position in row.items(): 33 | player_number = 1 if position in player_starting_positions[1] else 2 if position in player_starting_positions[2] else None 34 | 35 | if (player_number): 36 | pieces.append(self.create_piece(player_number, position)) 37 | 38 | self.board.pieces = pieces 39 | 40 | def create_piece(self, player_number, position): 41 | piece = Piece() 42 | piece.player = player_number 43 | piece.position = position 44 | piece.board = self.board 45 | 46 | return piece -------------------------------------------------------------------------------- /checkers/board_searcher.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | class BoardSearcher: 4 | 5 | def build(self, board): 6 | self.board = board 7 | self.uncaptured_pieces = list(filter(lambda piece: not piece.captured, board.pieces)) 8 | self.open_positions = [] 9 | self.filled_positions = [] 10 | self.player_positions = {} 11 | self.player_pieces = {} 12 | self.position_pieces = {} 13 | 14 | self.build_filled_positions() 15 | self.build_open_positions() 16 | self.build_player_positions() 17 | self.build_player_pieces() 18 | self.build_position_pieces() 19 | 20 | def build_filled_positions(self): 21 | self.filled_positions = reduce((lambda open_positions, piece: open_positions + [piece.position]), self.uncaptured_pieces, []) 22 | 23 | def build_open_positions(self): 24 | self.open_positions = [position for position in range(1, self.board.position_count) if not position in self.filled_positions] 25 | 26 | def build_player_positions(self): 27 | self.player_positions = { 28 | 1: reduce((lambda positions, piece: positions + ([piece.position] if piece.player == 1 else [])), self.uncaptured_pieces, []), 29 | 2: reduce((lambda positions, piece: positions + ([piece.position] if piece.player == 2 else [])), self.uncaptured_pieces, []) 30 | } 31 | 32 | def build_player_pieces(self): 33 | self.player_pieces = { 34 | 1: reduce((lambda pieces, piece: pieces + ([piece] if piece.player == 1 else [])), self.uncaptured_pieces, []), 35 | 2: reduce((lambda pieces, piece: pieces + ([piece] if piece.player == 2 else [])), self.uncaptured_pieces, []) 36 | } 37 | 38 | def build_position_pieces(self): 39 | self.position_pieces = {piece.position: piece for piece in self.uncaptured_pieces} 40 | 41 | def get_pieces_by_player(self, player_number): 42 | return self.player_pieces[player_number] 43 | 44 | def get_positions_by_player(self, player_number): 45 | return self.player_positions[player_number] 46 | 47 | def get_pieces_in_play(self): 48 | return self.player_pieces[self.board.player_turn] if not self.board.piece_requiring_further_capture_moves else [self.board.piece_requiring_further_capture_moves] 49 | 50 | def get_piece_by_position(self, position): 51 | return self.position_pieces.get(position) -------------------------------------------------------------------------------- /checkers/game.py: -------------------------------------------------------------------------------- 1 | from .board import Board 2 | 3 | class Game: 4 | 5 | def __init__(self): 6 | self.board = Board() 7 | self.moves = [] 8 | self.consecutive_noncapture_move_limit = 40 9 | self.moves_since_last_capture = 0 10 | 11 | def move(self, move): 12 | if move not in self.get_possible_moves(): 13 | raise ValueError('The provided move is not possible') 14 | 15 | self.board = self.board.create_new_board_from_move(move) 16 | self.moves.append(move) 17 | self.moves_since_last_capture = 0 if self.board.previous_move_was_capture else self.moves_since_last_capture + 1 18 | 19 | return self 20 | 21 | def move_limit_reached(self): 22 | return self.moves_since_last_capture >= self.consecutive_noncapture_move_limit 23 | 24 | def is_over(self): 25 | return self.move_limit_reached() or not self.get_possible_moves() 26 | 27 | def get_winner(self): 28 | if self.whose_turn() == 1 and not self.board.count_movable_player_pieces(1): 29 | return 2 30 | elif self.whose_turn() == 2 and not self.board.count_movable_player_pieces(2): 31 | return 1 32 | else: 33 | return None 34 | 35 | def get_possible_moves(self): 36 | return self.board.get_possible_moves() 37 | 38 | def whose_turn(self): 39 | return self.board.player_turn 40 | -------------------------------------------------------------------------------- /checkers/piece.py: -------------------------------------------------------------------------------- 1 | from math import ceil 2 | 3 | class Piece: 4 | 5 | def __init__(self): 6 | self.player = None 7 | self.other_player = None 8 | self.king = False 9 | self.captured = False 10 | self.position = None 11 | self.board = None 12 | self.capture_move_enemies = {} 13 | self.reset_for_new_board() 14 | 15 | def reset_for_new_board(self): 16 | self.possible_capture_moves = None 17 | self.possible_positional_moves = None 18 | 19 | def is_movable(self): 20 | return (self.get_possible_capture_moves() or self.get_possible_positional_moves()) and not self.captured 21 | 22 | def capture(self): 23 | self.captured = True 24 | self.position = None 25 | 26 | def move(self, new_position): 27 | self.position = new_position 28 | self.king = self.king or self.is_on_enemy_home_row() 29 | 30 | def get_possible_capture_moves(self): 31 | if self.possible_capture_moves == None: 32 | self.possible_capture_moves = self.build_possible_capture_moves() 33 | 34 | return self.possible_capture_moves 35 | 36 | def build_possible_capture_moves(self): 37 | adjacent_enemy_positions = list(filter((lambda position: position in self.board.searcher.get_positions_by_player(self.other_player)), self.get_adjacent_positions())) 38 | capture_move_positions = [] 39 | 40 | for enemy_position in adjacent_enemy_positions: 41 | enemy_piece = self.board.searcher.get_piece_by_position(enemy_position) 42 | position_behind_enemy = self.get_position_behind_enemy(enemy_piece) 43 | 44 | if (position_behind_enemy != None) and self.board.position_is_open(position_behind_enemy): 45 | capture_move_positions.append(position_behind_enemy) 46 | self.capture_move_enemies[position_behind_enemy] = enemy_piece 47 | 48 | return self.create_moves_from_new_positions(capture_move_positions) 49 | 50 | def get_position_behind_enemy(self, enemy_piece): 51 | current_row = self.get_row() 52 | current_column = self.get_column() 53 | enemy_column = enemy_piece.get_column() 54 | enemy_row = enemy_piece.get_row() 55 | column_adjustment = -1 if current_row % 2 == 0 else 1 56 | column_behind_enemy = current_column + column_adjustment if current_column == enemy_column else enemy_column 57 | row_behind_enemy = enemy_row + (enemy_row - current_row) 58 | 59 | return self.board.position_layout.get(row_behind_enemy, {}).get(column_behind_enemy) 60 | 61 | def get_possible_positional_moves(self): 62 | if self.possible_positional_moves == None: 63 | self.possible_positional_moves = self.build_possible_positional_moves() 64 | 65 | return self.possible_positional_moves 66 | 67 | def build_possible_positional_moves(self): 68 | new_positions = list(filter((lambda position: self.board.position_is_open(position)), self.get_adjacent_positions())) 69 | 70 | return self.create_moves_from_new_positions(new_positions) 71 | 72 | def create_moves_from_new_positions(self, new_positions): 73 | return [[self.position, new_position] for new_position in new_positions] 74 | 75 | def get_adjacent_positions(self): 76 | return self.get_directional_adjacent_positions(forward = True) + (self.get_directional_adjacent_positions(forward = False) if self.king else []) 77 | 78 | def get_column(self): 79 | return (self.position - 1) % self.board.width 80 | 81 | def get_row(self): 82 | return self.get_row_from_position(self.position) 83 | 84 | def is_on_enemy_home_row(self): 85 | return self.get_row() == self.get_row_from_position(1 if self.other_player == 1 else self.board.position_count) 86 | 87 | def get_row_from_position(self, position): 88 | return ceil(position / self.board.width) - 1 89 | 90 | def get_directional_adjacent_positions(self, forward): 91 | positions = [] 92 | current_row = self.get_row() 93 | next_row = current_row + ((1 if self.player == 1 else -1) * (1 if forward else -1)) 94 | 95 | if not next_row in self.board.position_layout: 96 | return [] 97 | 98 | next_column_indexes = self.get_next_column_indexes(current_row, self.get_column()) 99 | 100 | return [self.board.position_layout[next_row][column_index] for column_index in next_column_indexes] 101 | 102 | def get_next_column_indexes(self, current_row, current_column): 103 | column_indexes = [current_column, current_column + 1] if current_row % 2 == 0 else [current_column - 1, current_column] 104 | 105 | return filter((lambda column_index: column_index >= 0 and column_index < self.board.width), column_indexes) 106 | 107 | def __setattr__(self, name, value): 108 | super(Piece, self).__setattr__(name, value) 109 | 110 | if name == 'player': 111 | self.other_player = 1 if value == 2 else 2 -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Fork from the `dev` branch. Also submit your PR to the `dev` branch. PRs that are submitted to the `master` branch will be closed immediately. Merge the latest changes from the `dev` branch before you submit the pull request. If you have a request that can't be automatically merged, you may be asked to marge the latest changes and resubmit it. If applicable, add documentation for your changes to the relevant section in the readme. Also if your change affects the interface, please submit any necessary unit tests along with your PR. -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | A Python3 library that you can use to play a game of checkers/draughts. This is just a set of classes that you can use in your code, it's not an interactive shell checkersgame. 2 | 3 | - **Version:** 1.4.2 4 | 5 | [![Build Status](https://travis-ci.org/ImparaAI/checkers.png?branch=master)](https://travis-ci.org/ImparaAI/checkers) 6 | 7 | # Assumptions 8 | 9 | The rules used are for competitive American checkers or English draughts. This means an 8x8 board with force captures and regular kings. 10 | 11 | Each position on the board is numbered 1 to 32. Each move is represented as an array with two values: starting position and ending position. So if you're starting a new game, one of the available moves is `[9, 13]` for player 1. If there's a capture move, the ending position is the position the capturing piece will land on (i.e. two rows from its original row), which might look like `[13, 22]`. 12 | 13 | Each piece movement is completely distinct, even if the move is part of a multiple capture series. In [Portable Draughts Notation](https://en.wikipedia.org/wiki/Portable_Draughts_Notation) mutli-capture series are usually represented by a `5-32` (for a particularly long series of jumps), but in certain situations there could be multiple pathways to achieve that final position. This game requires an explicit spelling out of each distinct move in the multi-capture series. 14 | 15 | # Usage 16 | 17 | Create a new game: 18 | 19 | ```python 20 | from checkers.game import Game 21 | 22 | game = Game() 23 | ``` 24 | 25 | See whose turn it is: 26 | 27 | ```python 28 | game.whose_turn() #1 or 2 29 | ``` 30 | 31 | Get the possible moves: 32 | 33 | ```python 34 | game.get_possible_moves() #[[9, 13], [9, 14], [10, 14], [10, 15], [11, 15], [11, 16], [12, 16]] 35 | ``` 36 | 37 | Make a move: 38 | 39 | ```python 40 | game.move([9, 13]) 41 | ``` 42 | 43 | Check if the game is over: 44 | 45 | ```python 46 | game.is_over() #True or False 47 | ``` 48 | 49 | Find out who won: 50 | 51 | ```python 52 | game.get_winner() #None or 1 or 2 53 | ``` 54 | 55 | Review the move history: 56 | 57 | ```python 58 | game.moves #[[int, int], [int, int], ...] 59 | ``` 60 | 61 | Change the consecutive noncapture move limit (default `40` according to the [rules](http://www.usacheckers.com/rulesofcheckers.php)): 62 | 63 | ```python 64 | game.consecutive_noncapture_move_limit = 20 65 | game.move_limit_reached() #True or False 66 | ``` 67 | 68 | Review the pieces on the board: 69 | 70 | ```python 71 | for piece in game.board.pieces: 72 | piece.player #1 or 2 73 | piece.other_player #1 or 2 74 | piece.king #True or False 75 | piece.captured #True or False 76 | piece.position #1-32 77 | piece.get_possible_capture_moves() #[[int, int], [int, int], ...] 78 | piece.get_possible_positional_moves() #[[int, int], [int, int], ...] 79 | ``` 80 | 81 | # Testing 82 | 83 | Run `python3 -m unittest discover` from the root. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("readme.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name = "imparaai-checkers", 8 | version = "1.4.2", 9 | license = 'MIT', 10 | author = "ImparaAI", 11 | author_email = "author@example.com", 12 | description = "Library for playing a standard game of checkers/draughts", 13 | long_description = long_description, 14 | long_description_content_type = "text/markdown", 15 | url = "https://github.com/ImparaAI/checkers", 16 | packages = setuptools.find_packages(), 17 | classifiers = [ 18 | "Programming Language :: Python :: 3", 19 | "License :: OSI Approved :: MIT License", 20 | "Operating System :: OS Independent", 21 | ], 22 | ) -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImparaAI/checkers/4c41ce84b7b2693846c75110d23cb185580bc805/test/__init__.py -------------------------------------------------------------------------------- /test/test_game_over.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from checkers.game import Game 3 | 4 | import random 5 | 6 | class TestGameOver(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.game = Game() 10 | 11 | def test_new_game_not_over(self): 12 | self.expect(False) 13 | 14 | def test_win_by_capture(self): 15 | self.make_non_final_moves([[10, 14], [23, 18], [14, 23], [26, 19], [11, 15], [19, 10], [6, 15], [22, 18], [15, 22], [25, 18], [9, 13], [21, 17], [13, 22], 16 | [31, 26], [22, 31], [24, 19], [31, 24], [24, 15], [15, 22], [29, 25], [22, 29], [30, 25], [29, 22], [28, 24], [12, 16], [32, 27], [16, 20], [27, 23], 17 | [20, 27], [23, 18]]) 18 | 19 | self.move([22, 15]).expect(True) 20 | 21 | def test_win_by_no_legal_moves(self): 22 | self.make_non_final_moves([[11, 15], [22, 18], [15, 22], [25, 18], [12, 16], [18, 14], [9, 18], [23, 14], [10, 17], [21, 14], [5, 9], [14, 5], [6, 9], 23 | [29, 25], [9, 13], [25, 22], [2, 6], [22, 18], [13, 17], [27, 23], [17, 21], [24, 19], [8, 12], [30, 25], [21, 30], [28, 24], [4, 8], [18, 14], [6, 10], 24 | [32, 27], [10, 17], [23, 18], [16, 23], [23, 32], [24, 19], [30, 23], [23, 14], [31, 27], [32, 23]]) 25 | 26 | self.move([23, 16]).expect(True) 27 | 28 | def test_move_limit_draw(self): 29 | self.make_non_final_moves([[10, 14], [22, 17], [9, 13], [17, 10], [7, 14], [25, 22], [6, 10], [29, 25], [1, 6], [22, 18], [6, 9], [24, 19], [2, 6], [28, 24], 30 | [11, 16], [24, 20], [8, 11], [32, 28], [4, 8], [27, 24], [3, 7], [31, 27], [13, 17], [25, 22], [9, 13], [18, 9], [9, 2], [10, 14], [22, 18], [5, 9], [19, 15], 31 | [16, 19], [23, 16], [12, 19], [30, 25], [14, 23], [23, 32], [21, 14], [14, 5], [11, 18], [2, 11], [11, 4], [19, 23], [26, 19], [13, 17], [25, 21], [17, 22], 32 | [21, 17], [22, 25], [17, 14], [18, 22], [5, 1], [22, 26], [4, 8], [26, 31], [19, 15], [25, 30], [8, 11], [31, 26], [1, 6], [26, 23], [24, 19], [23, 16], 33 | [16, 7], [14, 10], [7, 14], [15, 10], [14, 7], [28, 24], [32, 28], [20, 16], [28, 19], [19, 12], [6, 9], [7, 10], [9, 13], [10, 7], [13, 9], [7, 3], [9, 6], 34 | [3, 7], [6, 1], [7, 11], [1, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], 35 | [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6]]) 36 | 37 | self.move([11, 8]).expect(True) 38 | 39 | def make_non_final_moves(self, moves): 40 | for move in moves: 41 | self.move(move).expect(False) 42 | 43 | def move(self, move): 44 | self.game.move(move) 45 | return self 46 | 47 | def expect(self, value): 48 | self.assertIs(self.game.is_over(), value) -------------------------------------------------------------------------------- /test/test_possible_moves.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from checkers.game import Game 3 | 4 | class TestPossibleMoves(unittest.TestCase): 5 | 6 | def test_possible_moves(self): 7 | self.game = Game() 8 | 9 | self.expect([[9, 13], [9, 14], [10, 14], [10, 15], [11, 15], [11, 16], [12, 16]]).move([10, 14]) 10 | self.expect([[21, 17], [22, 17], [22, 18], [23, 18], [23, 19], [24, 19], [24, 20]]).move([23, 18]) 11 | self.expect([[14, 23]]).move([14, 23]) 12 | self.expect([[26, 19], [27, 18]]).move([27, 18]) 13 | self.expect([[6, 10], [7, 10], [9, 13], [9, 14], [11, 15], [11, 16], [12, 16]]).move([9, 13]) 14 | self.expect([[18, 14], [18, 15], [21, 17], [22, 17], [24, 19], [24, 20], [26, 23], [31, 27], [32, 27]]).move([21, 17]) 15 | self.expect([[5, 9], [6, 9], [6, 10], [7, 10], [11, 15], [11, 16], [12, 16]]).move([6, 10]) 16 | self.expect([[17, 14], [18, 14], [18, 15], [24, 19], [24, 20], [25, 21], [26, 23], [31, 27], [32, 27]]).move([18, 14]) 17 | self.expect([[1, 6], [2, 6], [5, 9], [10, 15], [11, 15], [11, 16], [12, 16]]).move([2, 6]) 18 | self.expect([[14, 9], [22, 18], [24, 19], [24, 20], [25, 21], [26, 23], [31, 27], [32, 27]]).move([31, 27]) 19 | self.expect([[5, 9], [6, 9], [10, 15], [11, 15], [11, 16], [12, 16]]).move([11, 16]) 20 | self.expect([[14, 9], [22, 18], [24, 19], [24, 20], [25, 21], [26, 23], [27, 23]]).move([22, 18]) 21 | self.expect([[13, 22]]).move([13, 22]) 22 | self.expect([[22, 31]]).move([22, 31]) #double jump where 10-17 is also in a jumpable position if not for piece restriction 23 | self.expect([[14, 9], [18, 15], [24, 19], [24, 20], [25, 21], [25, 22], [27, 23], [30, 26]]).move([24, 19]) 24 | self.expect([[10, 17], [16, 23], [31, 24]]).move([31, 24]) 25 | self.expect([[24, 15]]).move([24, 15]) 26 | self.expect([[15, 22]]).move([15, 22]) 27 | self.expect([[25, 18]]).move([25, 18]) 28 | self.expect([[10, 17]]).move([10, 17]) 29 | self.expect([[18, 14], [18, 15], [28, 24], [29, 25], [30, 25], [30, 26], [32, 27]]).move([29, 25]) 30 | self.expect([[5, 9], [6, 9], [6, 10], [7, 10], [7, 11], [8, 11], [16, 19], [16, 20], [17, 21], [17, 22]]).move([17, 21]) 31 | self.expect([[18, 14], [18, 15], [25, 22], [28, 24], [30, 26], [32, 27]]).move([30, 26]) 32 | self.expect([[21, 30]]).move([21, 30]) 33 | self.expect([[18, 14], [18, 15], [26, 22], [26, 23], [28, 24], [32, 27]]).move([18, 15]) 34 | self.expect([[30, 23]]).move([30, 23]) 35 | self.expect([[15, 10], [15, 11], [28, 24], [32, 27]]).move([15, 11]) 36 | self.expect([[8, 15]]).move([8, 15]) 37 | self.expect([[28, 24], [32, 27]]).move([28, 24]) 38 | self.expect([[3, 8], [4, 8], [5, 9], [6, 9], [6, 10], [7, 10], [7, 11], [15, 18], [15, 19], [16, 19], [16, 20], [23, 26], [23, 27], [23, 18], [23, 19]]).move([4, 8]) 39 | self.expect([[24, 19], [24, 20], [32, 27], [32, 28]]).move([24, 19]) 40 | self.expect([[15, 24]]).move([15, 24]) 41 | self.expect([[32, 27], [32, 28]]).move([32, 27]) 42 | self.expect([[23, 32], [24, 31]]).move([23, 32]) 43 | self.expect([]) 44 | 45 | def move(self, move): 46 | self.game.move(move) 47 | 48 | def expect(self, expected_possible_moves): 49 | self.assertEqual(self.game.get_possible_moves(), expected_possible_moves) 50 | return self -------------------------------------------------------------------------------- /test/test_winner.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from checkers.game import Game 3 | 4 | import random 5 | 6 | class TestWinner(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.game = Game() 10 | 11 | def test_player_1_wins(self): 12 | self.make_non_winning_moves([[11, 15], [21, 17], [8, 11], [25, 21], [4, 8], [29, 25], [12, 16], [22, 18], [15, 22], [22, 29], [30, 25], [29, 22], [22, 13], [23, 18], 13 | [8, 12], [26, 23], [16, 20], [31, 26], [3, 8], [24, 19], [10, 14], [21, 17], [13, 22], [22, 31], [31, 24], [24, 15], [15, 22], [32, 27], [9, 13], [23, 18], 14 | [14, 23], [23, 32], [28, 24]]) 15 | 16 | self.move([20, 27]).expect(1) 17 | 18 | def test_player_2_wins(self): 19 | self.make_non_winning_moves([[10, 14], [22, 17], [9, 13], [17, 10], [6, 15], [23, 18], [15, 22], [25, 18], [13, 17], [21, 14], [5, 9], [14, 5], [1, 6], [5, 1], 20 | [11, 15], [1, 10], [10, 19], [12, 16], [19, 12], [7, 10], [26, 23], [10, 14], [18, 9], [3, 7], [12, 3], [3, 10], [2, 6], [9, 2], [4, 8], [2, 7], [8, 11]]) 21 | 22 | self.move([7, 16]).expect(2) 23 | 24 | def test_win_by_no_legal_moves(self): 25 | self.make_non_winning_moves([[11, 15], [22, 18], [15, 22], [25, 18], [12, 16], [18, 14], [9, 18], [23, 14], [10, 17], [21, 14], [5, 9], [14, 5], [6, 9], 26 | [29, 25], [9, 13], [25, 22], [2, 6], [22, 18], [13, 17], [27, 23], [17, 21], [24, 19], [8, 12], [30, 25], [21, 30], [28, 24], [4, 8], [18, 14], [6, 10], 27 | [32, 27], [10, 17], [23, 18], [16, 23], [23, 32], [24, 19], [30, 23], [23, 14], [31, 27], [32, 23]]) 28 | 29 | self.move([23, 16]).expect(1) 30 | 31 | def test_draw(self): 32 | self.make_non_winning_moves([[10, 14], [22, 17], [9, 13], [17, 10], [7, 14], [25, 22], [6, 10], [29, 25], [1, 6], [22, 18], [6, 9], [24, 19], [2, 6], [28, 24], 33 | [11, 16], [24, 20], [8, 11], [32, 28], [4, 8], [27, 24], [3, 7], [31, 27], [13, 17], [25, 22], [9, 13], [18, 9], [9, 2], [10, 14], [22, 18], [5, 9], [19, 15], 34 | [16, 19], [23, 16], [12, 19], [30, 25], [14, 23], [23, 32], [21, 14], [14, 5], [11, 18], [2, 11], [11, 4], [19, 23], [26, 19], [13, 17], [25, 21], [17, 22], 35 | [21, 17], [22, 25], [17, 14], [18, 22], [5, 1], [22, 26], [4, 8], [26, 31], [19, 15], [25, 30], [8, 11], [31, 26], [1, 6], [26, 23], [24, 19], [23, 16], 36 | [16, 7], [14, 10], [7, 14], [15, 10], [14, 7], [28, 24], [32, 28], [20, 16], [28, 19], [19, 12], [6, 9], [7, 10], [9, 13], [10, 7], [13, 9], [7, 3], [9, 6], 37 | [3, 7], [6, 1], [7, 11], [1, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], 38 | [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8], [6, 9], [8, 11], [9, 6], [11, 8]]) 39 | 40 | def make_non_winning_moves(self, moves): 41 | for move in moves: 42 | self.move(move).expect(None) 43 | 44 | def move(self, move): 45 | self.game.move(move) 46 | return self 47 | 48 | def expect(self, value): 49 | self.assertIs(self.game.get_winner(), value) --------------------------------------------------------------------------------