├── .coveragerc
├── .github
└── workflows
│ ├── build.yml
│ ├── code-cov.yml
│ ├── codeql-analysis.yml
│ └── tests.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── codecov.yml
├── draughts
├── PDN.py
├── __init__.py
├── ballot_files
│ ├── 11_english.json
│ ├── 11_italian.json
│ ├── 150russian_and_brazilian.json
│ ├── 2move_english.json
│ ├── 3move_english.json
│ ├── 4move_english.json
│ ├── 5move_english.json
│ ├── __init__.py
│ ├── brazilian.json
│ └── russian.json
├── ballots.py
├── convert.py
├── core
│ ├── __init__.py
│ ├── board.py
│ ├── board_initializer.py
│ ├── board_searcher.py
│ ├── game.py
│ ├── move.py
│ ├── piece.py
│ └── variant.py
├── engine.py
├── engines
│ ├── __init__.py
│ ├── checkerboard.py
│ ├── checkerboard_extra
│ │ ├── __init__.py
│ │ ├── engine_64.py
│ │ ├── engine_client.py
│ │ ├── engine_server.py
│ │ └── get_checker_board.py
│ ├── dxp.py
│ ├── dxp_communication
│ │ ├── __init__.py
│ │ ├── dxp_classes.py
│ │ └── dxp_communication.py
│ └── hub.py
├── svg.py
└── tournament.py
├── examples
├── board.svg
└── engine_pondering.py
├── other_licenses
├── ImparaAI checkers LICENSE
├── akalverboer DXC100_draughts_client LICENSE
└── fishnet LICENSE.txt
├── pyproject.toml
├── setup.cfg
└── test_pydraughts
├── __init__.py
├── conftest.py
├── test_ambiguous_moves.py
├── test_ballots.py
├── test_board.py
├── test_convert.py
├── test_dxp_communication.py
├── test_engines.py
├── test_game.py
├── test_move.py
├── test_pdn.py
├── test_piece.py
├── test_repr.py
├── test_svg.py
├── test_tournament.py
└── test_variants.py
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | # It is run through msl-loadlib, so coverage doesn't detect it.
4 | draughts/engines/checkerboard_extra/engine_server.py
5 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build the library.
2 |
3 | name: Build
4 |
5 | on:
6 | push:
7 | branches: [ main ]
8 | pull_request:
9 | branches: [ main ]
10 |
11 | jobs:
12 | build:
13 |
14 | runs-on: ${{ matrix.os }}
15 | strategy:
16 | matrix:
17 | os: [ubuntu-latest, macos-latest, windows-latest]
18 | python: ["3.13"]
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up Python ${{ matrix.python }}
23 | uses: actions/setup-python@v4
24 | with:
25 | python-version: ${{ matrix.python }}
26 | - name: Install dependencies
27 | if: ${{ matrix.os == 'windows-latest' }}
28 | run: |
29 | py -m pip install --upgrade pip
30 | py -m pip install --upgrade build
31 | - name: Install dependencies
32 | if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }}
33 | run: |
34 | python3 -m pip install --upgrade pip
35 | python3 -m pip install --upgrade build
36 | - name: Build
37 | if: ${{ matrix.os == 'windows-latest' }}
38 | run: |
39 | py -m build
40 | - name: Build
41 | if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }}
42 | run: |
43 | python3 -m build
44 |
--------------------------------------------------------------------------------
/.github/workflows/code-cov.yml:
--------------------------------------------------------------------------------
1 | name: CodeCov
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | run:
7 | runs-on: windows-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | with:
11 | fetch-depth: ‘2’
12 |
13 | - name: Setup Python
14 | uses: actions/setup-python@v4
15 | with:
16 | python-version: "3.13"
17 | - name: Generate Report
18 | run: |
19 | pip install msl-loadlib pytest pytest-timeout pytest-cov requests
20 | pytest --cov=draughts --cov-report=xml
21 | coverage report --show-missing
22 | - name: Upload Coverage to Codecov
23 | uses: codecov/codecov-action@v2
24 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '35 11 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'python' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
37 | # Learn more:
38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
39 |
40 | steps:
41 | - name: Checkout repository
42 | uses: actions/checkout@v3
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v2
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v2
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v2
72 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Tests
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | test:
14 |
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | matrix:
18 | os: [ubuntu-latest, macos-latest, windows-latest]
19 | python: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Set up Python ${{ matrix.python }}
24 | uses: actions/setup-python@v4
25 | with:
26 | python-version: ${{ matrix.python }}
27 | - name: Install dependencies
28 | run: |
29 | python -m pip install --upgrade pip
30 | pip install flake8 msl-loadlib pytest pytest-timeout requests
31 | - name: Lint with flake8
32 | run: |
33 | # stop the build if there are Python syntax errors or undefined names
34 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
35 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
36 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
37 | - name: Test with pytest
38 | run: |
39 | pytest --log-cli-level=10
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .venv/
2 | .idea/
3 | __pycache__
4 | draughts/__pycache__
5 | draughts/core/__pycache__
6 | draughts/engines/__pycache__
7 | draughts/engines/dxp_communication/__pycache__
8 | draughts/engines/checkerboard_extra/__pycache__
9 | draughts/engines/checkerboard_extra/engine_name.txt
10 | .gitattributes
11 |
12 | *.swp
13 | *.py[cod]
14 | *~
15 | .coverage
16 | .coveralls.yml
17 | nosetests.xml
18 | .tox
19 |
20 | dist
21 | build
22 | pydraughts.egg-info
23 | draughts/pydraughts.egg-info
24 | docs/_build
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021-2024 Ioannis Pantidis
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include draughts/ballot_files *.json
2 | recursive-include examples *
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pydraughts
2 | [](https://badge.fury.io/py/pydraughts) [](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/tests.yml) [](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/build.yml) [](https://github.com/AttackingOrDefending/pydraughts/actions/workflows/codeql-analysis.yml) [](https://codecov.io/gh/AttackingOrDefending/pydraughts)
3 |
4 | pydraughts is a draughts (checkers) library for Python with move generation, SVG visualizations, PDN reading and writing, engine communication and balloted openings. It is based on [ImparaAI/checkers](https://github.com/ImparaAI/checkers).
5 |
6 | Installing
7 | ----------
8 |
9 | Download and install the latest release:
10 |
11 | pip install pydraughts
12 |
13 | ## Features
14 |
15 | **Variants:**
16 | * Standard (International)
17 | * Frisian
18 | * frysk!
19 | * Antidraughts
20 | * Breakthrough
21 | * Russian
22 | * Brazilian
23 | * English/American
24 | * Italian
25 | * Turkish
26 |
27 | **Engine protocols:**
28 | * Hub
29 | * DXP
30 | * CheckerBoard
31 |
32 | **PDN Reading and Writing**
33 |
34 | * Import pydraughts
35 | ```python
36 | from draughts import Board, Move, WHITE, BLACK
37 | ```
38 | * Create a game
39 | ```python
40 | board = Board(variant="standard", fen="startpos")
41 | ```
42 | * Make a move
43 | ```python
44 | move = Move(board, steps_move=[34, 30])
45 | board.push(move)
46 |
47 | # Multi-capture
48 | board2 = Board(fen="W:WK40:B19,29")
49 | board2.push(Move(board2, pdn_move='40x14'))
50 | ```
51 | * Get a visual representation of the board as SVG
52 | ```python
53 | from draughts import svg
54 | svg.create_svg(Board(fen="B:W16,19,33,34,47,K4:B17,25,26"))
55 | ```
56 | 
57 | * Get a visual representation of the board in the terminal
58 | ```python
59 | print(board)
60 |
61 | """
62 | | b | | b | | b | | b | | b
63 | ---------------------------------------
64 | b | | b | | b | | b | | b |
65 | ---------------------------------------
66 | | b | | b | | b | | b | | b
67 | ---------------------------------------
68 | b | | b | | b | | b | | b |
69 | ---------------------------------------
70 | | | | | | | | | |
71 | ---------------------------------------
72 | | | | | | | | | w |
73 | ---------------------------------------
74 | | w | | w | | w | | | | w
75 | ---------------------------------------
76 | w | | w | | w | | w | | w |
77 | ---------------------------------------
78 | | w | | w | | w | | w | | w
79 | ---------------------------------------
80 | w | | w | | w | | w | | w |
81 | """
82 | ```
83 | * Get legal moves
84 | ```python
85 | moves = board.legal_moves()
86 | ```
87 | * Detect wins and draws
88 | ```python
89 | has_white_won = board.winner() == WHITE
90 | is_draw = board.winner() == 0
91 | winnner = board.winner()
92 | is_game_over = board.is_over()
93 | ```
94 | * Convert move to other types
95 | ```python
96 | move = Move(board, board_move=moves[0].board_move).pdn_move
97 | ```
98 | * Get fen
99 | ```python
100 | fen = game.fen
101 | ```
102 | * Communicate with engines
103 | ```python
104 | from draughts.engine import HubEngine, Limit
105 | engine = HubEngine(["scan.exe", "hub"])
106 | engine.init()
107 | limit = Limit(time=10)
108 | engine_move = engine.play(board, limit, ponder=False)
109 | ```
110 | * Read PDN games
111 | ```python
112 | from draughts.PDN import PDNReader
113 | games = PDNReader(filename=filepath)
114 | game = games.games[0]
115 | moves = game.moves
116 | ```
117 | * Write PDN games
118 | ```python
119 | from draughts.PDN import PDNWriter
120 | games = PDNWriter(filename=filepath, board=board)
121 | ```
122 | * Get a ballot
123 | ```python
124 | from draughts.ballots import Ballots
125 | ballots = Ballots('english')
126 | ballot1 = ballots.get_ballot()
127 | ballot2 = ballots.get_ballot()
128 | ```
129 | * Run tournaments
130 | ```python
131 | from draughts.tournament import RoundRobin
132 | player1 = (["scan.exe", "hub"], "hub", {}, None)
133 | player2 = ("kr_hub.exe", "hub", {}, None)
134 | players = [player1, player2]
135 | tournament = RoundRobin("tournament.pdn", players, start_time=20, increment=0.2, games_per_pair=4)
136 | scores = tournament.play()
137 | print(scores)
138 | tournament.print_standings()
139 | ```
140 |
141 | ## Example Engines
142 | Some engines that can be used with `pydraughts`.
143 |
144 | | Engine | Protocol |
145 | |:-------------------------------------------------------------------------------------------|:-------------|
146 | | [Kingsrow (international)](https://edgilbert.org/InternationalDraughts/download_links.htm) | Hub & DXP |
147 | | [Scan](https://hjetten.home.xs4all.nl/scan/scan.html) | Hub & DXP |
148 | | [Moby Dam](https://hjetten.home.xs4all.nl/mobydam/mobydam.html) | DXP |
149 | | [Kingsrow (english)](https://edgilbert.org/EnglishCheckers/KingsRowEnglish.htm) | CheckerBoard |
150 | | [Kingsrow (italian)](https://edgilbert.org/ItalianCheckers/KingsRowItalian.htm) | CheckerBoard |
151 | | [Cake](https://www.fierz.ch/download.php) | CheckerBoard |
152 | | [Kallisto](https://www.igorkorshunov.narod.ru/Draughts/Kallisto4.rar) | CheckerBoard |
153 |
154 | ## Selected Projects
155 | If you like, share your interesting project that uses pydraughts.
156 |
157 | | Projects |
158 | |-----------------------------------------------------------------------|
159 | | Checkers Reinforcement Learning — https://github.com/d3da/checkers-rl |
160 |
161 | ## Acknowledgements
162 | Thanks to [fishnet](https://github.com/lichess-org/fishnet/tree/ebd2a5e16d37135509cbfbff9998e0b798866ef5) which was modified to add support for Hub engines. Thanks to [akalverboer](https://github.com/akalverboer) for their [DXC100_draughts_client](https://github.com/akalverboer/DXC100_draughts_client) which was modified to add support for DXP engines.
163 |
164 | ## License
165 | pydraughts is licensed under The MIT License. Check out `LICENSE` for the full text.
166 | The licenses of the other projects that pydraughts uses are in the `other_licenses` folder.
167 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | target: auto
6 | threshold: 10%
7 | patch:
8 | default:
9 | target: auto
10 | threshold: 10%
11 |
--------------------------------------------------------------------------------
/draughts/PDN.py:
--------------------------------------------------------------------------------
1 | import re
2 | import string
3 | from draughts.convert import fen_to_variant
4 | from draughts import Board, Move
5 | from typing import List, Optional, Dict, Union
6 |
7 |
8 | class _PDNGame:
9 | """Read one PDN game."""
10 | def __init__(self, pdn_text: str) -> None:
11 | self.values_to_variant = {20: "standard", 21: "english", 22: "italian", 23: "american pool", 24: "spanish",
12 | 25: "russian", 26: "brazilian", 27: "canadian", 28: "portuguese", 29: "czech",
13 | 30: "turkish", 31: "thai", 40: "frisian", 41: "spantsiretti"}
14 | self.tags: Dict[str, str] = {}
15 | self.moves: List[str] = []
16 | self.variant: Optional[str] = None
17 | self.notation: Optional[int] = None
18 | self.notation_type: Optional[str] = None
19 | self.game_ending = '*'
20 | self.pdn_text = pdn_text
21 | self._rest_of_games: List[str] = []
22 | self._read()
23 |
24 | def _read(self) -> None:
25 | """Read a PDN game."""
26 | lines = self.pdn_text.split('\n')
27 | tag_lines = []
28 | last_tag_line = -1
29 | for index, line in enumerate(lines):
30 | if line.startswith('['):
31 | tag_lines.append(line)
32 | last_tag_line = index
33 | elif re.sub(r'\s', '', line):
34 | break
35 |
36 | for tag_line in tag_lines:
37 | line = tag_line[1:-1]
38 | quote_index = line.index('"')
39 | name = line[:quote_index - 1]
40 | value = line[quote_index + 1:-1]
41 | self.tags[name] = value
42 |
43 | rest_of_games = []
44 | last_move_line = -1
45 | move_lines = []
46 | for index, line in enumerate(lines[last_tag_line + 1:]):
47 | split_line = re.split(r'[\s|\]](1-0|1/2-1/2|0-1|2-0|1-1|0-2|0-0|\*)[\s|\[]', ' ' + line + ' ', maxsplit=1)
48 | if len(split_line) == 3:
49 | move_lines.append(split_line[0])
50 | last_move_line = index
51 | self.game_ending = split_line[1]
52 | rest_of_games.append(split_line[2])
53 | break
54 | if re.sub(r'\s', '', line):
55 | move_lines.append(line)
56 | last_move_line = index
57 |
58 | rest_of_games += lines[last_tag_line + 1 + last_move_line + 1:]
59 |
60 | str_moves = " ".join(move_lines)
61 |
62 | # Changes to the PDN.
63 |
64 | # From https://stackoverflow.com/a/37538815/10014873
65 | def remove_text_between_parens(text: str) -> str:
66 | n = 1 # Run at least once.
67 | while n:
68 | text, n = re.subn(r'\([^()]*\)', '', text) # Remove non-nested/flat balanced parts.
69 | return text
70 |
71 | def remove_text_between_brackets(text: str) -> str:
72 | n = 1 # Run at least once.
73 | while n:
74 | text, n = re.subn(r'{[^{}]*}', '', text) # Remove non-nested/flat balanced parts.
75 | return text
76 |
77 | str_moves = remove_text_between_parens(str_moves)
78 | str_moves = remove_text_between_brackets(str_moves)
79 | str_moves = re.sub(r" +", " ", str_moves)
80 | str_moves = re.sub(r"\$[0-9]+", "", str_moves)
81 | str_moves = str_moves.replace('. ...', '...')
82 | str_moves = str_moves.replace('...', '.')
83 | str_moves = str_moves.replace('?', '')
84 | str_moves = str_moves.replace('!', '')
85 | str_moves = str_moves.replace('. ', '.')
86 | str_moves = str_moves.replace('- ', '-')
87 | str_moves = str_moves.replace('x ', 'x')
88 | str_moves = str_moves.replace(': ', ':')
89 |
90 | move_numbers = re.findall(r"\d+\.", str_moves)
91 | double_numbers = list(set(filter(lambda move: move_numbers.count(move) >= 2, move_numbers)))
92 | for move_number in double_numbers:
93 | str_moves = (str_moves[:str_moves.index(move_number) + len(move_number)] +
94 | str_moves[str_moves.index(move_number) + len(move_number):].replace(move_number, ""))
95 |
96 | moves = str_moves.split(".")[1:]
97 | if not moves:
98 | return
99 | starts = self.tags.get('FEN', 'W')
100 | if starts.startswith('W'):
101 | clean_moves = []
102 | list_moves = [move.split()[:2] for move in moves]
103 | for move in list_moves:
104 | clean_moves.extend(move)
105 | else:
106 | clean_moves = [moves[0].split()[0]]
107 | list_moves = [[moves[0].split()[0]]] + [move.split()[:2] for move in moves[1:]]
108 | for move in list_moves:
109 | clean_moves.extend(move)
110 | results = ["1-0", "1/2-1/2", "0-1", "2-0", "1-1", "0-2", "0-0", "*"]
111 | clean_moves = clean_moves[:-1] if clean_moves[-1] in results else clean_moves
112 | self.moves = clean_moves
113 |
114 | if "GameType" in self.tags:
115 | game_type = self.tags["GameType"]
116 | values = game_type.split(',')
117 | variant_number = int(values[0])
118 | self.variant = self.values_to_variant.get(variant_number, None)
119 | if len(values) == 6:
120 | notation = values[4]
121 | self.notation_type = notation[0].lower()
122 | self.notation = int(notation[1])
123 | else: # Try to guess the variant.
124 | board_10 = ['31', '32', '33', '34', '35', '16', '17', '18', '19', '20']
125 | board_8 = ['21', '22', '23', '24', '9', '09', '10', '11', '12']
126 | first_move = moves[0]
127 | if list(filter(lambda move: first_move.startswith(move), board_10)):
128 | self.variant = "standard"
129 | elif list(filter(lambda move: first_move.startswith(move), board_8)):
130 | self.variant = "english"
131 | elif first_move[0] in string.ascii_letters:
132 | self.variant = "russian"
133 |
134 | self._rest_of_games = rest_of_games
135 |
136 | def get_titles(self) -> List[str]:
137 | """Get player titles."""
138 | return [self.tags.get("WhiteTitle", ""), self.tags.get("BlackTitle", "")]
139 |
140 | def get_ratings(self) -> List[str]:
141 | """Get player ratings."""
142 | return [self.tags.get("WhiteRating", ""), self.tags.get("BlackRating", "")]
143 |
144 | def get_na(self) -> List[str]:
145 | """Get player network address."""
146 | return [self.tags.get("WhiteNA", ""), self.tags.get("BlackNA", "")]
147 |
148 | def get_types(self) -> List[str]:
149 | """Get player types (human, computer, etc.)."""
150 | return [self.tags.get("WhiteType", ""), self.tags.get("BlackType", "")]
151 |
152 | def _get_rest_of_games(self) -> str:
153 | """Get the rest of the games."""
154 | # This class only reads the first game. You can get the rest with this function.
155 | return '\n'.join(self._rest_of_games)
156 |
157 |
158 | class PDNReader:
159 | """Read PDN games."""
160 | def __init__(self, pdn_text: Optional[str] = None, filename: Optional[str] = None,
161 | encodings: Union[List[str], str, None] = None) -> None:
162 | if encodings is None:
163 | encodings = ['utf8', 'ISO 8859/1']
164 | if type(encodings) == str:
165 | encodings = [encodings]
166 | if filename:
167 | pdn_text = ''
168 | for encoding in encodings:
169 | try:
170 | with open(filename, encoding=encoding) as pdn_file:
171 | pdn_text = pdn_file.read()
172 | break
173 | except Exception:
174 | pass
175 | assert pdn_text is not None
176 | self.pdn_text = pdn_text
177 | self.pdn_text = re.sub('\n +', '\n', self.pdn_text)
178 | self.pdn_text = re.sub('\n\n+', '\n\n', self.pdn_text)
179 | self.games = []
180 | game = _PDNGame(self.pdn_text)
181 | self.games.append(game)
182 | more_games = game._get_rest_of_games()
183 | while re.sub(r'\s', '', more_games):
184 | game = _PDNGame(more_games)
185 | self.games.append(game)
186 | more_games = game._get_rest_of_games()
187 |
188 |
189 | class PDNWriter:
190 | """Write a game to a file."""
191 | VARIANT_TO_GAMETYPE = {'standard': 20, 'english': 21, 'italian': 22, 'russian': 25, 'brazilian': 26, 'turkish': 30,
192 | 'frisian': 40, 'frysk!': 40}
193 | SHORT_TO_LONG_GAMETYPE = {'20': '20,W,10,10,N2,0', '21': '21,B,8,8,N1,0', '22': '22,W,8,8,N2,1',
194 | '25': '25,W,8,8,A0,0', '26': '26,W,8,8,A0,0', '30': '30,W,8,8,A0,0', '40': '40,W,10,10,N2,0'}
195 |
196 | def __init__(self, filename: str, board: Optional[Board] = None, moves: Union[List[str], List[Move], None] = None,
197 | variant: Optional[str] = None, starting_fen: Optional[str] = None,
198 | tags: Optional[Dict[str, Union[str, int]]] = None, game_ending: str = '*',
199 | replay_moves_from_board: bool = True, file_encoding: str = 'utf8', file_mode: str = 'a') -> None:
200 | """
201 | :param replay_moves_from_board: The already saved pdn_move in move_stack may be wrong because it is pseudolegal
202 | and doesn't account for ambiguous moves. If replay_moves_from_board is enabled, it will replay all the moves to
203 | find the correct representation of them.
204 | """
205 | assert board or moves is not None
206 | self.pdn_text = ''
207 | self.notation_type: Optional[str] = None
208 | self.notation: Optional[int] = None
209 |
210 | self.board = board
211 | self.moves: Union[List[str], List[Move]]
212 | if self.board:
213 | self.moves = self.board.move_stack
214 | self.variant = self.board.variant
215 | self.starting_fen = self.board.initial_fen
216 | self.tags = tags or {}
217 | else:
218 | self.moves = moves
219 | self.variant = variant or 'standard'
220 | self.starting_fen = starting_fen or self._startpos_to_fen()
221 | self.tags = tags or {}
222 | self.game_ending = game_ending
223 |
224 | self.replay_moves_from_board = replay_moves_from_board
225 | self.filename = filename
226 | self.file_encoding = file_encoding
227 | self.file_mode = file_mode
228 | self._fix_ambiguous_moves()
229 | self._write()
230 |
231 | def _fix_ambiguous_moves(self) -> None:
232 | """Replay the moves to fix any ambiguous PDN move."""
233 | if self.moves and type(self.moves[0]) == str:
234 | return
235 | game = Board(self.variant, self.starting_fen)
236 | correct_moves = []
237 | for move in self.moves:
238 | correct_move = Move(game, board_move=move.board_move)
239 | correct_moves.append(correct_move)
240 | game.push(correct_move)
241 | self.moves = correct_moves
242 |
243 | def _write(self) -> None:
244 | """Write the PDN file."""
245 | pdn_text = ''
246 | if 'GameType' not in self.tags:
247 | short_gametype = str(self.VARIANT_TO_GAMETYPE.get(self.variant.lower(), 20))
248 | long_gametype = self.SHORT_TO_LONG_GAMETYPE[short_gametype]
249 | self.tags['GameType'] = long_gametype
250 | if 'FEN' not in self.tags:
251 | self.tags['FEN'] = self.starting_fen
252 | for tag in self.tags:
253 | pdn_text += f'[{tag} "{self.tags[tag]}"]\n'
254 | pdn_text += '\n'
255 |
256 | standard_moves = []
257 | for move in self.moves:
258 | if type(move) != str:
259 | standard_move = move.pdn_move
260 | else:
261 | standard_move = move
262 |
263 | standard_moves.append(standard_move)
264 |
265 | if len(standard_moves) == 1 and self.starting_fen[0] == 'W':
266 | pdn_text += f'1. {standard_moves[0]}'
267 | standard_moves = []
268 | elif standard_moves:
269 | if self.starting_fen[0] == 'W':
270 | pdn_text += f'1. {standard_moves[0]} {standard_moves[1]}'
271 | standard_moves = standard_moves[2:]
272 | else:
273 | pdn_text += f'1... {standard_moves[0]}'
274 | standard_moves = standard_moves[1:]
275 |
276 | write_move_number = True
277 | move_number = 2
278 | while standard_moves:
279 | move_number_to_write = f'{move_number}. ' if write_move_number else ''
280 | pdn_text += f' {move_number_to_write}{standard_moves.pop(0)}'
281 | write_move_number = not write_move_number
282 | if write_move_number:
283 | move_number += 1
284 | pdn_text += f' {self.game_ending}'
285 | pdn_text += '\n\n'
286 |
287 | self.pdn_text = pdn_text
288 |
289 | if self.filename:
290 | with open(self.filename, self.file_mode, encoding=self.file_encoding) as file:
291 | file.write(self.pdn_text)
292 |
293 | def _startpos_to_fen(self) -> str:
294 | """Get the starting fen."""
295 | if self.variant == 'frysk!':
296 | fen = 'W:W46,47,48,49,50:B1,2,3,4,5'
297 | elif self.variant == 'turkish':
298 | fen = 'W:W41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56:B9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24'
299 | elif self.variant in ['brazilian', 'russian', 'english', 'italian']:
300 | fen = 'W:W21,22,23,24,25,26,27,28,29,30,31,32:B1,2,3,4,5,6,7,8,9,10,11,12'
301 | else:
302 | fen = 'W:W31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20'
303 | return fen_to_variant(fen, self.variant)
304 |
--------------------------------------------------------------------------------
/draughts/__init__.py:
--------------------------------------------------------------------------------
1 | from draughts.core.variant import Board, Move, WHITE, BLACK
2 |
3 | __all__ = ["Board", "Move", "WHITE", "BLACK"]
4 |
5 | __author__ = "Ioannis Pantidis"
6 | __copyright__ = "2021-2025, " + __author__
7 | __version__ = "0.6.7"
8 |
--------------------------------------------------------------------------------
/draughts/ballot_files/2move_english.json:
--------------------------------------------------------------------------------
1 | {
2 | "all": {"01 9-13 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "02 9-13 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "03 9-13 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "04 9-13 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "05 9-13 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "06 9-13 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "07 9-13 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,12,13,2,3,4,5,6,7,8", "08 9-14 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "09 9-14 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "10 9-14 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "11 9-14 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "12 9-14 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "13 9-14 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "14 9-14 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,5,6,7,8", "15 10-14 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "16 10-14 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "17 10-14 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "18 10-14 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "19 10-14 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "20 10-14 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "21 10-14 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,11,12,14,2,3,4,5,6,7,8,9", "22 10-15 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "23 10-15 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "24 10-15 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "25 10-15 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "26 10-15 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "27 10-15 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "28 10-15 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,11,12,15,2,3,4,5,6,7,8,9", "29 11-15 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "30 11-15 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "31 11-15 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "32 11-15 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "33 11-15 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "34 11-15 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "35 11-15 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,10,12,15,2,3,4,5,6,7,8,9", "36 11-16 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "37 11-16 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "38 11-16 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "39 11-16 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "40 11-16 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "41 11-16 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "42 11-16 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,10,12,16,2,3,4,5,6,7,8,9", "43 12-16 21-17": "B:W17,22,23,24,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "44 12-16 22-17": "B:W17,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "45 12-16 22-18": "B:W18,21,23,24,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "46 12-16 23-18": "B:W18,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "47 12-16 23-19": "B:W19,21,22,24,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "48 12-16 24-19": "B:W19,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9", "49 12-16 24-20": "B:W20,21,22,23,25,26,27,28,29,30,31,32:B1,10,11,16,2,3,4,5,6,7,8,9"},
3 | "standard": ["01 9-13 21-17", "02 9-13 22-17", "03 9-13 22-18", "04 9-13 23-18", "05 9-13 23-19", "06 9-13 24-19", "07 9-13 24-20", "09 9-14 22-17", "10 9-14 22-18", "12 9-14 23-19", "13 9-14 24-19", "14 9-14 24-20", "16 10-14 22-17", "17 10-14 22-18", "19 10-14 23-19", "20 10-14 24-19", "21 10-14 24-20", "22 10-15 21-17", "23 10-15 22-17", "24 10-15 22-18", "25 10-15 23-18", "26 10-15 23-19", "27 10-15 24-19", "28 10-15 24-20", "29 11-15 21-17", "30 11-15 22-17", "31 11-15 22-18", "32 11-15 23-18", "33 11-15 23-19", "34 11-15 24-19", "35 11-15 24-20", "36 11-16 21-17", "37 11-16 22-17", "38 11-16 22-18", "39 11-16 23-18", "41 11-16 24-19", "42 11-16 24-20", "43 12-16 21-17", "44 12-16 22-17", "45 12-16 22-18", "46 12-16 23-18", "48 12-16 24-19", "49 12-16 24-20"],
4 | "lost": ["08 9-14 21-17", "11 9-14 23-18", "15 10-14 21-17", "18 10-14 23-18", "40 11-16 23-19", "47 12-16 23-19"]
5 | }
--------------------------------------------------------------------------------
/draughts/ballot_files/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/draughts/ballot_files/__init__.py
--------------------------------------------------------------------------------
/draughts/ballots.py:
--------------------------------------------------------------------------------
1 | # 11 english, 11 italian, 4 move english and 5 move english ballots are from
2 | # Ed Gilbert (http://edgilbert.org/Checkers/KingsRow.htm).
3 |
4 | import random
5 | import json
6 | import os
7 | from typing import Tuple, List, Dict
8 |
9 |
10 | class Ballots:
11 | def __init__(self, variant: str, moves: int = 3, eleven_pieces: bool = False, basic_positions: bool = False,
12 | include_lost_games: bool = False) -> None:
13 | self.variant = variant
14 | self.moves = moves
15 | self.eleven_pieces = eleven_pieces
16 | self.basic_positions = basic_positions
17 | self.include_lost_games = include_lost_games
18 | self.filename = self._find_file()
19 | self.positions, self.keys = self.open_file()
20 | self.keys_to_use = self.keys.copy()
21 |
22 | def _find_file(self) -> str:
23 | """Get the filename of the ballots."""
24 | if self.variant == 'italian':
25 | return '11_italian.json'
26 | if self.variant == 'english':
27 | if self.eleven_pieces:
28 | return '11_english.json'
29 | if self.moves == 2:
30 | return '2move_english.json'
31 | if self.moves == 4:
32 | return '4move_english.json'
33 | if self.moves == 5:
34 | return '5move_english.json'
35 | if self.basic_positions:
36 | return '150russian_and_brazilian.json'
37 | if self.variant == 'russian':
38 | return 'russian.json'
39 | if self.variant == 'brazilian':
40 | return 'brazilian.json'
41 | return '3move_english.json'
42 |
43 | def open_file(self) -> Tuple[Dict[str, str], List[str]]:
44 | """Open the ballot file."""
45 | filepath = os.path.join(os.path.dirname(__file__), 'ballot_files', self.filename)
46 | with open(filepath) as file:
47 | data = json.load(file)
48 | keys = data['standard'] + (data.get('lost', []) if self.include_lost_games else [])
49 | return data['all'], keys
50 |
51 | def get_ballot(self) -> str:
52 | """Get one ballot."""
53 | if not self.keys_to_use:
54 | self.keys_to_use = self.keys.copy()
55 | key = self.keys_to_use.pop(random.randint(0, len(self.keys_to_use) - 1))
56 | return self.positions[key]
57 |
--------------------------------------------------------------------------------
/draughts/convert.py:
--------------------------------------------------------------------------------
1 | from math import ceil
2 | import string
3 | import re
4 | from typing import Tuple, Optional, List
5 |
6 |
7 | def _get_squares(variant: Optional[str]) -> Tuple[int, int, int, bool]:
8 | """Returns the total squares, squares per row, squares per column and if every other square is playable."""
9 | # The default values are for International draughts.
10 | total_squares = 50
11 | squares_per_row = 5
12 | squares_per_column = 5
13 | every_other_square = True
14 | if variant in ['english', 'italian', 'russian', 'brazilian']:
15 | total_squares = 32
16 | squares_per_row = 4
17 | squares_per_column = 4
18 | elif variant in ['turkish']:
19 | total_squares = 64
20 | squares_per_row = 8
21 | squares_per_column = 8
22 | every_other_square = False
23 | return total_squares, squares_per_row, squares_per_column, every_other_square
24 |
25 |
26 | def _rotate_move(internal_move: str, notation: Optional[int] = None, variant: Optional[str] = None) -> str:
27 | """Rotate the move."""
28 | separators = ['-', 'x', ':']
29 | split_move = []
30 | correct_seperator = ''
31 | for separator in separators:
32 | split_move = internal_move.split(separator)
33 | if split_move[0] != internal_move:
34 | correct_seperator = separator
35 | break
36 | int_move_parts = list(map(int, split_move))
37 | variant_to_notation = {'standard': 2, 'english': 1, 'italian': 2, 'russian': 0, 'brazilian': 0, 'turkish': 0,
38 | 'frisian': 2, 'frysk!': 2, 'antidraughts': 2, 'breakthrough': 2}
39 | if notation is None:
40 | notation = variant_to_notation.get(variant, 2)
41 |
42 | def reverse_column(split_int_move: List[int]) -> List[int]:
43 | per_row = _get_squares(variant)[1]
44 | for index, square in enumerate(split_int_move):
45 | square_in_row = square % per_row
46 | if square_in_row == 0:
47 | square_in_row += per_row
48 | split_int_move[index] = ((square - 1) // per_row) * per_row + (per_row - (square_in_row - 1))
49 | return split_int_move
50 |
51 | def reverse_row_and_column(split_int_move: List[int]) -> List[int]:
52 | squares = _get_squares(variant)[0]
53 | split_int_move = list(map(lambda square: squares + 1 - square, split_int_move))
54 | return split_int_move
55 |
56 | def reverse_row(split_int_move: List[int]) -> List[int]:
57 | return reverse_column(reverse_row_and_column(split_int_move))
58 |
59 | if notation == 0:
60 | rotated_move = reverse_row(int_move_parts)
61 | elif notation == 1:
62 | rotated_move = reverse_row_and_column(int_move_parts)
63 | elif notation == 2:
64 | rotated_move = int_move_parts
65 | else: # notation == 3
66 | rotated_move = reverse_column(int_move_parts)
67 |
68 | rotated_str_move = list(map(str, rotated_move))
69 | return correct_seperator.join(rotated_str_move)
70 |
71 |
72 | def _algebraic_to_number(algebraic_move: str, squares_per_letter: Optional[int] = None, variant: Optional[str] = None,
73 | every_other_square: Optional[bool] = None) -> str:
74 | """Convert an algebraic move to a numeric move."""
75 | if every_other_square is None:
76 | if variant == 'turkish':
77 | every_other_square = False
78 | else:
79 | every_other_square = True
80 | algebraic_notation = algebraic_move[0] in string.ascii_letters
81 | if not algebraic_notation:
82 | return algebraic_move
83 | algebraic_move = algebraic_move.lower()
84 | if squares_per_letter is None:
85 | squares_per_letter = _get_squares(variant)[2]
86 |
87 | separators = ['-', 'x', ':']
88 | special_seperators = [r'([a-zA-z]+\d+)']
89 | split_move = []
90 | correct_seperator = ''
91 | for separator in separators:
92 | split_move = list(filter(bool, re.split(separator, algebraic_move)))
93 | if split_move[0] != algebraic_move:
94 | correct_seperator = separator
95 | break
96 |
97 | if not correct_seperator:
98 | for separator in special_seperators:
99 | split_move = list(filter(bool, re.split(separator, algebraic_move)))
100 | if split_move[0] != algebraic_move:
101 | correct_seperator = '-'
102 | break
103 |
104 | numeric_move = []
105 | for move in split_move:
106 | numeric_move.append(_algebraic_to_numeric_square(move, squares_per_letter, every_other_square=every_other_square))
107 | numeric_str_move = list(map(str, numeric_move))
108 | return correct_seperator.join(numeric_str_move)
109 |
110 |
111 | def _algebraic_to_numeric_square(square: str, squares_per_letter: int, every_other_square: bool = True) -> int:
112 | """Convert an algebraic square to a numeric square."""
113 | algebraic_notation = square[0] in string.ascii_letters
114 | if not algebraic_notation:
115 | return int(square)
116 | if not every_other_square:
117 | return (int(square[1]) - 1) * squares_per_letter + string.ascii_lowercase.index(square[0]) + 1
118 | return (int(square[1]) - 1) * squares_per_letter + ceil(string.ascii_lowercase.index(square[0]) // 2) + 1
119 |
120 |
121 | def _number_to_algebraic(number_move: str, width: Optional[int] = None, variant: Optional[str] = None,
122 | every_other_square: Optional[bool] = None) -> str:
123 | """Convert a numeric move to an algebraic move."""
124 | if every_other_square is None:
125 | every_other_square = _get_squares(variant)[3]
126 | algebraic_notation = number_move[0] in string.ascii_letters
127 | if algebraic_notation:
128 | return number_move
129 | if width is None:
130 | width = _get_squares(variant)[1]
131 |
132 | separators = ['-', 'x', ':']
133 | split_move = []
134 | correct_seperator = ''
135 | for separator in separators:
136 | split_move = list(filter(bool, re.split(separator, number_move)))
137 | if split_move[0] != number_move:
138 | correct_seperator = separator
139 | break
140 |
141 | algebraic_move = []
142 | for move_part in split_move:
143 | algebraic_move.append(_numeric_to_algebraic_square(move_part, width, every_other_square=every_other_square))
144 | return correct_seperator.join(algebraic_move)
145 |
146 |
147 | def _numeric_to_algebraic_square(square: str, width: int, every_other_square: Optional[bool] = True) -> str:
148 | """Convert a numeric square to an algebraic square."""
149 | algebraic_notation = square[0] in string.ascii_letters
150 | if algebraic_notation:
151 | return square
152 | int_square = int(square)
153 | row = ceil(int_square / width) - 1
154 | column = (int_square - 1) % width
155 | if every_other_square:
156 | column *= 2
157 | column += 1 if row % 2 == 1 else 0
158 | return string.ascii_lowercase[column] + str(row + 1)
159 |
160 |
161 | def _change_fen_from_variant(li_fen: str, notation: Optional[int] = None, squares_per_letter: int = 5,
162 | every_other_square: bool = True, variant: Optional[str] = None) -> str:
163 | """Convert an internal fen to the correct fen for the variant."""
164 | if variant:
165 | _, _, squares_per_letter, every_other_square = _get_squares(variant)
166 |
167 | fen = li_fen.split(':')
168 | if len(fen) < 3:
169 | return li_fen
170 | starts = fen[0]
171 | white_pieces = fen[1][1:].split(',')
172 | black_pieces = fen[2][1:].split(',')
173 | white_pieces = list(filter(bool, white_pieces))
174 | black_pieces = list(filter(bool, black_pieces))
175 |
176 | white_pieces_remove_hyphen = []
177 | for white_piece in white_pieces:
178 | if '-' in white_piece:
179 | start_end = white_piece.split('-')
180 | add_for_king = ''
181 | if start_end[0][0] == 'K':
182 | add_for_king = 'K'
183 | start_end[0] = start_end[0][1:]
184 | start = _algebraic_to_numeric_square(start_end[0], squares_per_letter, every_other_square)
185 | end = _algebraic_to_numeric_square(start_end[1], squares_per_letter, every_other_square)
186 | for number in range(start, end + 1):
187 | white_pieces_remove_hyphen.append(add_for_king + _rotate_move(str(number), notation=notation, variant=variant))
188 | else:
189 | add_for_king = ''
190 | if white_piece[0] == 'K':
191 | add_for_king = 'K'
192 | white_piece = white_piece[1:]
193 | white_pieces_remove_hyphen.append(
194 | add_for_king + _rotate_move(
195 | str(_algebraic_to_numeric_square(white_piece, squares_per_letter, every_other_square)),
196 | notation=notation, variant=variant))
197 |
198 | black_pieces_remove_hyphen = []
199 | for black_piece in black_pieces:
200 | if '-' in black_piece:
201 | start_end = black_piece.split('-')
202 | add_for_king = ''
203 | if start_end[0][0] == 'K':
204 | add_for_king = 'K'
205 | start_end[0] = start_end[0][1:]
206 | start = _algebraic_to_numeric_square(start_end[0], squares_per_letter, every_other_square)
207 | end = _algebraic_to_numeric_square(start_end[1], squares_per_letter, every_other_square)
208 | for number in range(start, end + 1):
209 | black_pieces_remove_hyphen.append(add_for_king + _rotate_move(str(number), notation=notation, variant=variant))
210 | else:
211 | add_for_king = ''
212 | if black_piece[0] == 'K':
213 | add_for_king = 'K'
214 | black_piece = black_piece[1:]
215 | black_pieces_remove_hyphen.append(
216 | add_for_king + _rotate_move(
217 | str(_algebraic_to_numeric_square(black_piece, squares_per_letter, every_other_square)),
218 | notation=notation, variant=variant))
219 |
220 | # Because in english black starts.
221 | white_starts = variant not in ['english']
222 | if not white_starts:
223 | white_pieces_remove_hyphen, black_pieces_remove_hyphen = black_pieces_remove_hyphen, white_pieces_remove_hyphen
224 | starts = 'W' if starts == 'B' else 'B'
225 |
226 | white_pieces_remove_hyphen.sort()
227 | black_pieces_remove_hyphen.sort()
228 | return f'{starts}:W{",".join(white_pieces_remove_hyphen)}:B{",".join(black_pieces_remove_hyphen)}'
229 |
230 |
231 | def fen_from_variant(fen: str, variant: Optional[str] = None) -> str:
232 | """Convert variant fen to internal fen."""
233 | variant = variant.lower() if variant else variant
234 | fen = _change_fen_from_variant(fen, variant=variant)
235 | return fen
236 |
237 |
238 | def fen_to_variant(fen: str, variant: Optional[str] = None, to_algebraic: Optional[bool] = None) -> str:
239 | """Convert internal fen to variant fen."""
240 | variant = variant.lower() if variant else variant
241 | fen = _change_fen_from_variant(fen, variant=variant)
242 | if to_algebraic or variant in ['russian', 'brazilian', 'turkish']:
243 | new_white_pieces = []
244 | new_black_pieces = []
245 | white_pieces = fen.split(':')[1][1:].split(',')
246 | black_pieces = fen.split(':')[2][1:].split(',')
247 | white_pieces = list(filter(bool, white_pieces))
248 | black_pieces = list(filter(bool, black_pieces))
249 | for piece in white_pieces:
250 | add = ''
251 | if piece.lower().startswith('k'):
252 | add = 'K'
253 | piece = piece[1:]
254 | new_white_pieces.append(add + _number_to_algebraic(piece, variant=variant))
255 | for piece in black_pieces:
256 | add = ''
257 | if piece.lower().startswith('k'):
258 | add = 'K'
259 | piece = piece[1:]
260 | new_black_pieces.append(add + _number_to_algebraic(piece, variant=variant))
261 | fen = f'{fen[0]}:W{",".join(new_white_pieces)}:B{",".join(new_black_pieces)}'
262 | return fen
263 |
264 |
265 | def move_from_variant(move: str, variant: Optional[str] = None) -> str:
266 | """Convert variant PDN move to internal PDN move."""
267 | variant = variant.lower() if variant else variant
268 | move = _algebraic_to_number(move, variant=variant)
269 | move = _rotate_move(move, variant=variant)
270 | return move
271 |
272 |
273 | def move_to_variant(move: str, variant: Optional[str] = None, to_algebraic: Optional[bool] = None) -> str:
274 | """Convert internal PDN move to variant PDN move."""
275 | variant = variant.lower() if variant else variant
276 | move = _rotate_move(move, variant=variant)
277 | if to_algebraic is not False and variant in ['russian', 'brazilian', 'turkish']:
278 | move = _number_to_algebraic(move, variant=variant)
279 | return move
280 |
--------------------------------------------------------------------------------
/draughts/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/draughts/core/__init__.py
--------------------------------------------------------------------------------
/draughts/core/board.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from draughts.core.board_searcher import BoardSearcher
3 | from draughts.core.board_initializer import BoardInitializer
4 | from draughts.core.piece import Piece
5 | from functools import reduce
6 | import pickle
7 | from typing import Optional, List, Tuple, Any, Dict
8 |
9 | WHITE = 2
10 | BLACK = 1
11 |
12 |
13 | class Board:
14 |
15 | def __init__(self, variant: str = 'standard', fen: str = 'startpos') -> None:
16 | if fen != 'startpos':
17 | self.player_turn = WHITE if fen[0].lower() == 'w' else BLACK
18 | else:
19 | self.player_turn = WHITE
20 |
21 | if variant in ['brazilian', 'russian', 'english', 'italian']:
22 | self.width = 4
23 | self.height = 8
24 | elif variant == 'turkish':
25 | self.width = 8
26 | self.height = 8
27 | else:
28 | self.width = 5
29 | self.height = 10
30 |
31 | if variant == 'frysk!':
32 | self.rows_per_user_with_pieces = 1
33 | elif variant == 'turkish':
34 | self.rows_per_user_with_pieces = 2
35 | elif variant in ['brazilian', 'russian', 'english', 'italian']:
36 | self.rows_per_user_with_pieces = 3
37 | else:
38 | self.rows_per_user_with_pieces = 4
39 |
40 | self.position_count = self.width * self.height
41 | self.position_layout: Dict[int, Dict[int, int]] = {}
42 | self.piece_requiring_further_capture_moves: Optional[Piece] = None
43 | self.previous_move_was_capture = False
44 | self.variant = variant
45 | self.fen = fen
46 | self.searcher = BoardSearcher()
47 | BoardInitializer(self, self.fen).initialize()
48 |
49 | self.pieces_promote_and_stop_capturing = self.variant in ['english', 'italian']
50 | self.pieces_promote_and_continue_capturing = self.variant in ['russian']
51 |
52 | def count_movable_player_pieces(self, player_number: int = 1, captures: Optional[List[int]] = None) -> int:
53 | """Count the pieces of one player that can be moved."""
54 | if captures is None:
55 | captures = []
56 | return reduce((lambda count, piece: count + (1 if piece.is_movable(captures) else 0)),
57 | self.searcher.get_pieces_by_player(player_number), 0)
58 |
59 | def get_possible_moves(self, captures: List[int]) -> List[List[int]]:
60 | """Get all possible moves."""
61 | capture_moves = self.get_possible_capture_moves(captures)
62 |
63 | return capture_moves if capture_moves else self.get_possible_positional_moves()
64 |
65 | def get_possible_capture_moves(self, captures: List[int]) -> List[List[int]]:
66 | """Get all possible capture moves (not positional moves)."""
67 | return reduce((lambda moves, piece: moves + piece.get_possible_capture_moves(captures)),
68 | self.searcher.get_pieces_in_play(), [])
69 |
70 | def get_possible_positional_moves(self) -> List[List[int]]:
71 | """Get all possible positional moves (not capture moves)."""
72 | return reduce((lambda moves, piece: moves + piece.get_possible_positional_moves()),
73 | self.searcher.get_pieces_in_play(), [])
74 |
75 | def position_is_open(self, position: int) -> bool:
76 | """Get if the position is open (a piece is not in the given square)."""
77 | return position in self.searcher.open_positions
78 |
79 | def create_new_board_from_move(self, move: List[int], move_number: int, captures: List[int]
80 | ) -> Tuple[Board, Optional[int]]:
81 | """Create a new board and play the move given."""
82 | new_board = pickle.loads(pickle.dumps(self, -1)) # A lot faster that deepcopy.
83 | enemy_position = None
84 |
85 | if move in self.get_possible_capture_moves(captures):
86 | enemy_position = new_board.perform_capture_move(move, move_number, captures)
87 | else:
88 | new_board.perform_positional_move(move, move_number)
89 |
90 | return new_board, enemy_position
91 |
92 | def push_move(self, move: List[int], move_number: int, captures: List[int]) -> Tuple[Board, Optional[int]]:
93 | """Play the move given without creating a new board."""
94 | # It takes 40% less time than create_new_board_from_move (60% faster).
95 | enemy_position = None
96 |
97 | if move in self.get_possible_capture_moves(captures):
98 | enemy_position = self.perform_capture_move(move, move_number, captures)
99 | else:
100 | self.perform_positional_move(move, move_number)
101 |
102 | return self, enemy_position
103 |
104 | def perform_capture_move(self, move: List[int], move_number: int, captures: List[int]) -> Optional[int]:
105 | """Make a capture move."""
106 | self.previous_move_was_capture = True
107 | piece = self.searcher.get_piece_by_position(move[0])
108 | originally_was_king = piece.king
109 | enemy_piece = piece.capture_move_enemies[move[1]]
110 | enemy_position = enemy_piece.position
111 | enemy_piece.capture()
112 | self.move_piece(piece, move[1], move_number)
113 | if not originally_was_king and piece.king and self.pieces_promote_and_stop_capturing:
114 | further_capture_moves_for_piece = []
115 | elif not originally_was_king and not self.pieces_promote_and_continue_capturing:
116 | was_king = piece.king
117 | piece.king = False
118 | further_capture_moves_for_piece = [capture_move for capture_move in self.get_possible_capture_moves(
119 | captures + [enemy_position]) if move[1] == capture_move[0]]
120 | if not further_capture_moves_for_piece and was_king:
121 | piece.king = True
122 | else:
123 | further_capture_moves_for_piece = [capture_move for capture_move in self.get_possible_capture_moves(
124 | captures + [enemy_position]) if move[1] == capture_move[0]]
125 |
126 | if further_capture_moves_for_piece:
127 | self.piece_requiring_further_capture_moves = self.searcher.get_piece_by_position(move[1])
128 | else:
129 | self.piece_requiring_further_capture_moves = None
130 | self.switch_turn()
131 | return enemy_position
132 |
133 | def perform_positional_move(self, move: List[int], move_number: int) -> None:
134 | """Make a positional move."""
135 | self.previous_move_was_capture = False
136 | piece = self.searcher.get_piece_by_position(move[0])
137 | self.move_piece(piece, move[1], move_number)
138 | self.switch_turn()
139 |
140 | def switch_turn(self) -> None:
141 | """Switch the turn."""
142 | self.player_turn = BLACK if self.player_turn == WHITE else WHITE
143 |
144 | def move_piece(self, piece: Piece, to: int, move_number: int) -> None:
145 | """Move a piece."""
146 | piece.move(to, move_number)
147 | self.pieces = sorted(self.pieces, key=lambda piece: piece.position if not piece.captured else 0)
148 |
149 | def is_valid_row_and_column(self, row: int, column: int) -> bool:
150 | """Get if the given row and column is inside the board."""
151 | if row < 0 or row >= self.height:
152 | return False
153 |
154 | if column < 0 or column >= self.width:
155 | return False
156 |
157 | return True
158 |
159 | def __setattr__(self, name: str, value: Any) -> None:
160 | super(Board, self).__setattr__(name, value)
161 |
162 | if name == 'pieces':
163 | [piece.reset_for_new_board() for piece in self.pieces]
164 |
165 | self.searcher.build(self)
166 |
--------------------------------------------------------------------------------
/draughts/core/board_initializer.py:
--------------------------------------------------------------------------------
1 | from draughts.core.piece import Piece
2 | from typing import Any
3 |
4 | WHITE = 2
5 | BLACK = 1
6 |
7 |
8 | class BoardInitializer:
9 |
10 | def __init__(self, board: Any, fen: str = 'startpos') -> None:
11 | self.board = board
12 | self.fen = fen
13 |
14 | def initialize(self) -> None:
15 | """Initialize the board."""
16 | self.build_position_layout()
17 | self.set_starting_pieces()
18 |
19 | def build_position_layout(self) -> None:
20 | """Build the position layout."""
21 | self.board.position_layout = {}
22 | position = 1
23 |
24 | for row in range(self.board.height):
25 | self.board.position_layout[row] = {}
26 |
27 | for column in range(self.board.width):
28 | self.board.position_layout[row][column] = position
29 | position += 1
30 |
31 | def set_starting_pieces(self) -> None:
32 | """Create the pieces."""
33 | pieces = []
34 | if self.fen != 'startpos': # Hub fen
35 | # starting = self.fen[0]
36 | board = self.fen[1:]
37 | for index, position in enumerate(board):
38 | piece = None
39 | if position.lower() == 'w':
40 | # Index + 1 because enumerate returns 0-49 while the board takes 1-50.
41 | piece = self.create_piece(WHITE, index + 1)
42 | if position == 'W':
43 | piece.king = True
44 | elif position.lower() == 'b':
45 | piece = self.create_piece(BLACK, index + 1)
46 | if position == 'B':
47 | piece.king = True
48 | if piece:
49 | pieces.append(piece)
50 | else: # Not used, but it isn't removed, because it may be needed in the future
51 | starting_piece_count = self.board.width * self.board.rows_per_user_with_pieces
52 | player_starting_positions = {
53 | 1: list(range(1, starting_piece_count + 1)),
54 | 2: list(range(self.board.position_count - starting_piece_count + 1, self.board.position_count + 1))
55 | }
56 |
57 | for key, row in self.board.position_layout.items():
58 | for key, position in row.items():
59 | player_number = 1 if position in player_starting_positions[1] else (
60 | 2 if position in player_starting_positions[2] else None)
61 |
62 | if player_number:
63 | pieces.append(self.create_piece(player_number, position))
64 |
65 | self.board.pieces = pieces
66 |
67 | def create_piece(self, player_number: int, position: int) -> Piece:
68 | """Create a piece."""
69 | piece = Piece(position, player_number, self.board, variant=self.board.variant)
70 |
71 | return piece
72 |
--------------------------------------------------------------------------------
/draughts/core/board_searcher.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 | from functools import reduce
3 | from typing import List, Dict, Any
4 | from draughts.core.piece import Piece
5 |
6 | WHITE = 2
7 | BLACK = 1
8 |
9 |
10 | class BoardSearcher:
11 |
12 | def build(self, board: Any) -> None:
13 | """Build the searcher."""
14 | self.board = board
15 | self.uncaptured_pieces = list(filter(lambda piece: not piece.captured, board.pieces))
16 | self.open_positions: List[int] = []
17 | self.filled_positions: List[int] = []
18 | self.player_positions: Dict[int, List[int]] = {}
19 | self.player_pieces: Dict[int, List[Piece]] = {}
20 | self.position_pieces: Dict[int, Piece] = {}
21 |
22 | self.build_filled_positions()
23 | self.build_open_positions()
24 | self.build_player_positions()
25 | self.build_player_pieces()
26 | self.build_position_pieces()
27 |
28 | def build_filled_positions(self) -> None:
29 | """Find the filled positions (squares which have a piece)."""
30 | self.filled_positions = reduce((lambda open_positions, piece: open_positions + [piece.position]),
31 | self.uncaptured_pieces, [])
32 |
33 | def build_open_positions(self) -> None:
34 | """Find the open positions (empty squares)."""
35 | self.open_positions = list(set(range(1, self.board.position_count + 1)).difference(self.filled_positions))
36 |
37 | def build_player_positions(self) -> None:
38 | """Find the positions where each player has a piece."""
39 | self.player_positions = {
40 | 1: reduce((lambda positions, piece: positions + ([piece.position] if piece.player == BLACK else [])),
41 | self.uncaptured_pieces, []),
42 | 2: reduce((lambda positions, piece: positions + ([piece.position] if piece.player == WHITE else [])),
43 | self.uncaptured_pieces, [])
44 | }
45 |
46 | def build_player_pieces(self) -> None:
47 | """Find all the pieces of both players."""
48 | self.player_pieces = {
49 | BLACK: reduce((lambda pieces, piece: pieces + ([piece] if piece.player == BLACK else [])),
50 | self.uncaptured_pieces, []),
51 | WHITE: reduce((lambda pieces, piece: pieces + ([piece] if piece.player == WHITE else [])),
52 | self.uncaptured_pieces, [])
53 | }
54 |
55 | def build_position_pieces(self) -> None:
56 | """Make a dict where the key is the square and the value is the piece in this square."""
57 | self.position_pieces = {piece.position: piece for piece in self.uncaptured_pieces}
58 |
59 | def get_pieces_by_player(self, player_number: int) -> List[Piece]:
60 | """Get all the pieces of one player."""
61 | return self.player_pieces[player_number]
62 |
63 | def get_positions_by_player(self, player_number: int) -> List[int]:
64 | """Get the positions of one player's pieces."""
65 | return self.player_positions[player_number]
66 |
67 | def get_pieces_in_play(self) -> List[Piece]:
68 | """
69 | Get pieces in play. They are: All the pieces of the player playing now except when a piece is
70 | in the middle of a multi-capture, so it has already captured one or more pieces, but it can capture more,
71 | where we only return the piece that is in the middle of the multi-capture.
72 | """
73 | return (self.player_pieces[self.board.player_turn] if not self.board.piece_requiring_further_capture_moves else
74 | [self.board.piece_requiring_further_capture_moves])
75 |
76 | def get_piece_by_position(self, position: int) -> Piece:
77 | """Get the piece given its position."""
78 | return self.position_pieces[position]
79 |
--------------------------------------------------------------------------------
/draughts/engine.py:
--------------------------------------------------------------------------------
1 | from draughts.engines.dxp import DXPEngine
2 | from draughts.engines.hub import HubEngine
3 | from draughts.engines.checkerboard import CheckerBoardEngine
4 | from typing import Optional, Union, Dict, Any
5 | from draughts.core.variant import Move
6 |
7 |
8 | class Limit:
9 | """Conditions on when the engine should stop searching."""
10 | def __init__(self, time: Union[int, float, None] = None, inc: Union[int, float, None] = None,
11 | depth: Optional[int] = None, nodes: Optional[int] = None, movetime: Union[int, float, None] = None):
12 | assert time is not None or depth is not None or nodes is not None or movetime is not None
13 | self.time = time
14 | self.inc = inc
15 | self.depth = depth
16 | self.nodes = nodes
17 | self.movetime = movetime
18 |
19 |
20 | class PlayResult:
21 | """The outcome of the engine search."""
22 | def __init__(self, move: Optional[Move] = None, ponder: Optional[Move] = None, info: Optional[Dict[str, Any]] = None,
23 | draw_offered: bool = False, resigned: bool = False):
24 | self.move = move
25 | self.ponder = ponder
26 | self.info = info
27 | self.draw_offered = draw_offered
28 | self.resigned = resigned
29 |
30 |
31 | __all__ = ['HubEngine', 'DXPEngine', 'CheckerBoardEngine', 'Limit', 'PlayResult']
32 |
--------------------------------------------------------------------------------
/draughts/engines/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/draughts/engines/__init__.py
--------------------------------------------------------------------------------
/draughts/engines/checkerboard.py:
--------------------------------------------------------------------------------
1 | from draughts.engines.checkerboard_extra.engine_64 import Engine64
2 | from draughts.engines.checkerboard_extra.engine_client import Engine32
3 | import os
4 | import draughts
5 | import draughts.engine
6 | import logging
7 | from draughts.convert import move_to_variant
8 | from typing import Union, List, Any, Dict, Tuple
9 |
10 | logger = logging.getLogger("pydraughts")
11 |
12 |
13 | class CheckerBoardEngine:
14 | def __init__(self, command: Union[List[str], str], divide_time_by: int = 40, checkerboard_timing: bool = False,
15 | ENGINE: int = 5) -> None:
16 | if type(command) == str:
17 | command = [command]
18 | command = list(filter(bool, command))
19 | command = " ".join(command)
20 | if "\\" not in command and "/" not in command:
21 | self.cwd = os.path.realpath(os.path.expanduser("."))
22 | self.command = os.path.join(self.cwd, command)
23 | else:
24 | self.command = command
25 | self.ENGINE = ENGINE
26 | self.info = ""
27 | self.id = {}
28 | self.result = None
29 | self.divide_time_by = divide_time_by
30 | self.checkerboard_timing = checkerboard_timing
31 | self._sent_variant = False
32 | self.engine: Union[Engine64, Engine32]
33 | self.engine, self.bits = self._open_engine()
34 | self.id["name"] = self.engine.enginecommand('name')[0].decode()
35 |
36 | def _open_engine(self) -> Union[Tuple[Engine64, int], Tuple[Engine32, int]]:
37 | """Open the engine process."""
38 | try:
39 | return Engine64(self.command), 64
40 | except Exception:
41 | return Engine32(self.command), 32
42 |
43 | def setoption(self, name: str, value: Union[str, int]) -> None:
44 | """Set an engine option."""
45 | if name == 'divide-time-by':
46 | self.divide_time_by = int(value)
47 | else:
48 | self.engine.enginecommand(f"set {name} {value}")
49 |
50 | def configure(self, options: Dict[str, Union[str, int]]) -> None:
51 | """Configure many options at once."""
52 | for name, value in options.items():
53 | self.setoption(name, value)
54 |
55 | def kill_process(self) -> None:
56 | """Kill the engine process."""
57 | if self.bits == 32:
58 | self.engine.shutdown_server32()
59 | else:
60 | self.engine.kill_process()
61 |
62 | def play(self, board: draughts.Board, time_limit: Any) -> Any:
63 | """Engine search."""
64 | time = time_limit.time
65 | inc = time_limit.inc
66 | depth = time_limit.depth
67 | nodes = time_limit.nodes
68 | movetime = time_limit.movetime
69 |
70 | if not inc:
71 | inc = 0
72 |
73 | if not self._sent_variant:
74 | if board.variant == 'russian':
75 | self.engine.enginecommand('set gametype 25')
76 | elif board.variant == 'brazilian':
77 | self.engine.enginecommand('set gametype 26')
78 | elif board.variant == 'italian':
79 | self.engine.enginecommand('set gametype 22')
80 | elif board.variant == 'english':
81 | self.engine.enginecommand('set gametype 21')
82 | self._sent_variant = True
83 |
84 | if board.move_stack:
85 | reversible_moves = " ".join(list(map(lambda move: move.pdn_move, board._reversible_moves)))
86 | gamehist = f'set gamehist {board._last_non_reversible_fen} {reversible_moves}'
87 | if len(gamehist) > 256:
88 | gamehist = " ".join(gamehist[-256:].split()[1:])
89 | self.engine.enginecommand(gamehist)
90 |
91 | time_to_use = None
92 | if time:
93 |
94 | if time < 0 and inc < 0:
95 | time = 0
96 | inc = 0
97 | elif time < 0:
98 | inc = max(inc + time, 0)
99 | time = 0
100 | elif inc < 0:
101 | time = max(time + inc, 0)
102 | inc = 0
103 |
104 | if self.checkerboard_timing:
105 | if time < inc * .4:
106 | time_to_use = inc * .4
107 | elif time < inc:
108 | time_to_use = time
109 | else:
110 | time_to_use = inc + (time - inc) / 2.5
111 | else:
112 | time_to_use = time / self.divide_time_by
113 |
114 | logger.debug(f"Fen: {board.fen}, Time to use: {time_to_use}, Time: {time}, Inc: {inc}, Movetime: {movetime}")
115 |
116 | hub_pos_move, info, cbmove, result = self.engine.getmove(board, time_to_use, time, inc, movetime)
117 |
118 | logger.debug(f"Hub Pos Move: {hub_pos_move}, CBMove: {cbmove}, Info: {info.decode()}, Result: {result}")
119 |
120 | if hub_pos_move:
121 | hub_move = '-'.join([hub_pos_move[i:i+2] for i in range(0, len(hub_pos_move), 2)])
122 | bestmove = draughts.Move(board, hub_move=move_to_variant(hub_move, board.variant, to_algebraic=False))
123 | else:
124 | steps = []
125 | positions = [cbmove['from']]
126 | jumps = max(cbmove['jumps'], 1)
127 | for pos in cbmove['path'][1:jumps]:
128 | positions.append(pos)
129 | positions.append(cbmove['to'])
130 | for pos in positions:
131 | # Checkerboard returns first the column, then the row
132 | steps.append(self._row_col_to_num(board, pos[1], pos[0]))
133 |
134 | steps = list(map(lambda step: int(move_to_variant(str(step), board.variant, to_algebraic=False)), steps))
135 | bestmove = draughts.Move(board, steps_move=steps)
136 |
137 | self.info = info.decode()
138 | self.result = result
139 | return draughts.engine.PlayResult(bestmove, None, {'info': self.info, 'result': self.result})
140 |
141 | def _row_col_to_num(self, board: draughts.Board, row: int, col: int) -> int:
142 | """Get the square from the row and column."""
143 | if row % 2 == 0:
144 | col = int(((col + 2) / 2) - 1)
145 | else:
146 | col = int(((col + 1) / 2) - 1)
147 | # Because:
148 | # 1. In italian the bottom-left square isn't playable, so in CheckerBoard the board is flipped vertically.
149 | # 2. In most variants the bottom-left square for the starting side (usually white) is in column a,
150 | # while in english black starts, so the bottom-left square for the starting side (black) is in row h.
151 | flip_column = board.variant not in ['english', 'italian']
152 | if flip_column:
153 | col = board._game.board.width - 1 - col
154 | # Because in english black starts
155 | white_starts = board.variant not in ['english']
156 | if not white_starts:
157 | row = (board._game.board.height - 1) - row
158 | loc = board._game.board.position_layout.get(row, {}).get(col)
159 | assert isinstance(loc, int)
160 | return loc
161 |
--------------------------------------------------------------------------------
/draughts/engines/checkerboard_extra/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/draughts/engines/checkerboard_extra/__init__.py
--------------------------------------------------------------------------------
/draughts/engines/checkerboard_extra/engine_64.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | from ctypes import wintypes
3 | import os
4 | import draughts
5 | from draughts.engines.checkerboard_extra.get_checker_board import get_board, from_board
6 | from typing import Optional, Union, Tuple, Dict, Any
7 |
8 |
9 | class Engine64:
10 | def __init__(self, command: str) -> None:
11 | os.add_dll_directory(os.path.realpath(os.path.expanduser(os.path.dirname(command))))
12 | self.engine = ctypes.windll.LoadLibrary(command)
13 |
14 | def kill_process(self) -> None:
15 | """Kill the engine process."""
16 | handle = self.engine._handle
17 | try:
18 | # Windows
19 | ctypes.windll.kernel32.FreeLibrary.argtypes = [wintypes.HMODULE]
20 | ctypes.windll.kernel32.FreeLibrary(handle)
21 | except Exception:
22 | # Unix
23 | self.engine.dlcose(handle)
24 |
25 | def enginecommand(self, command: str) -> Tuple[bytes, int]:
26 | """Send an enginecommand to the engine."""
27 | output = ctypes.create_string_buffer(b'', 1024)
28 | result = self.engine.enginecommand(ctypes.create_string_buffer(bytes(command.encode('ascii')), 256), output)
29 | return output.value, result
30 |
31 | def getmove(self, game: draughts.Board, maxtime: Union[int, float, None] = None, time: Union[int, float, None] = None,
32 | increment: Union[int, float, None] = None, movetime: Union[int, float, None] = None
33 | ) -> Tuple[Optional[str], bytes, Dict[str, Any], int]:
34 | """Send a getmove to the engine."""
35 | assert maxtime is not None or time is not None or movetime is not None
36 |
37 | # From CheckerBoard API:
38 | WHITE = 1
39 | BLACK = 2
40 |
41 | board = get_board(game)
42 |
43 | # Reversed color because red (black) starts first and not white in english checkers in Checkerboard.
44 | color = WHITE if game.turn == draughts.WHITE else BLACK
45 |
46 | info = 0
47 | moreinfo = 0
48 | if movetime:
49 | info = info | (1 << 1) # 2nd bit means the engine has to think for exactly maxtime seconds
50 | elif time is not None and increment is not None:
51 | if time / .01 > 2 ** 15 - 1 or increment / .01 > 2 ** 15 - 1:
52 | info = info | (1 << 3) # 0.1 seconds
53 | info = info | (1 << 4) # 0.1 seconds
54 | time = int(time / .1)
55 | increment = int(increment / .1)
56 | elif time / .001 > 2 ** 15 - 1 or increment / .001 > 2 ** 15 - 1:
57 | info = info | (1 << 3) # 0.01 seconds
58 | time = int(time / .01)
59 | increment = int(increment / .01)
60 | else:
61 | info = info | (1 << 4) # 0.001 seconds
62 | time = int(time / .001)
63 | increment = int(increment / .001)
64 | bin_time = bin(time)[2:].zfill(16)
65 | if len(bin_time) > 16:
66 | bin_time = '1' * 16
67 | bin_inc = bin(increment)[2:].zfill(16)
68 | if len(bin_inc) > 16:
69 | bin_inc = '1' * 16
70 | moreinfo = eval('0b' + bin_time + bin_inc)
71 |
72 | if movetime is not None:
73 | maxtime_double = ctypes.c_double(float(movetime))
74 | else:
75 | assert maxtime is not None
76 | maxtime_double = ctypes.c_double(float(maxtime))
77 | output = ctypes.create_string_buffer(b'', 1024)
78 | playnow = ctypes.c_int(0)
79 |
80 | class coor(ctypes.Structure):
81 | _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)]
82 |
83 | class CBmove(ctypes.Structure):
84 | _fields_ = [("jumps", ctypes.c_int), ("newpiece", ctypes.c_int), ("oldpiece", ctypes.c_int), ("from", coor),
85 | ("to", coor), ("path", coor * 12), ("del", coor * 12), ("delpiece", ctypes.c_int * 12)]
86 |
87 | # self.engine.argtypes = [((ctypes.c_int * 8) * 8), ctypes.c_int, ctypes.c_double, (ctypes.c_char * 1024),
88 | # ctypes.POINTER(ctypes.c_int), ctypes.c_int, ctypes.c_int, ctypes.POINTER(CBmove)]
89 |
90 | cbmove = CBmove()
91 |
92 | result = self.engine.getmove(board, color, maxtime_double, output, ctypes.byref(playnow), info, moreinfo,
93 | ctypes.byref(cbmove))
94 |
95 | old_fen = game._game.get_fen()
96 | new_fen = from_board(board, game)
97 | our_pieces, opponents_pieces = (['w', 'W'], ['b', 'B']) if old_fen[0] == 'W' else (['b', 'B'], ['w', 'W'])
98 | captures = []
99 | start_pos, end_pos = None, None
100 | for index in range(1, len(old_fen)):
101 | if old_fen[index] in our_pieces and new_fen[index] == 'e':
102 | start_pos = index
103 | elif new_fen[index] in our_pieces and old_fen[index] == 'e':
104 | end_pos = index
105 | elif old_fen[index] in opponents_pieces and new_fen[index] == 'e':
106 | captures.append(index)
107 | hub_pos_move = None
108 | if start_pos and end_pos:
109 | hub_pos_move = game._game.make_len_2(start_pos) + game._game.make_len_2(end_pos) + game._game.sort_captures(
110 | captures)
111 |
112 | cbmove_output_2 = {}
113 | cbmove_output_2['jumps'] = cbmove.jumps
114 | cbmove_output_2['oldpiece'] = cbmove.oldpiece
115 | cbmove_output_2['newpiece'] = cbmove.newpiece
116 | cbmove_output_2['to'] = cbmove.to.x, cbmove.to.y
117 | cbmove.to = getattr(cbmove, 'from')
118 | cbmove_output_2['from'] = cbmove.to.x, cbmove.to.y
119 | cbmove_output_2['path'] = [(cbmove.path[i].x, cbmove.path[i].y) for i in range(12)]
120 | cbmove.path = getattr(cbmove, 'del')
121 | cbmove_output_2['del'] = [(cbmove.path[i].x, cbmove.path[i].y) for i in range(12)]
122 | cbmove_output_2['delpiece'] = [cbmove.delpiece[i] for i in range(12)]
123 | return hub_pos_move, output.value, cbmove_output_2, result
124 |
--------------------------------------------------------------------------------
/draughts/engines/checkerboard_extra/engine_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Optional, Union, Tuple, Dict, Any
3 | import draughts
4 |
5 | from msl.loadlib import Client64
6 |
7 |
8 | class Engine32(Client64):
9 | """64 bit client to communicate with the 32 bit server that is running the old Checkerboard engine."""
10 | def __init__(self, command: str) -> None:
11 |
12 | command = command.replace('\\', '\\\\')
13 |
14 | super(Engine32, self).__init__(module32='engine_server', append_sys_path=os.path.dirname(__file__), timeout=15.,
15 | dll_name=command)
16 |
17 | def enginecommand(self, command: str) -> Tuple[bytes, int]:
18 | """Send an enginecommand to the engine."""
19 | response: Tuple[bytes, int] = self.request32('enginecommand', command)
20 | return response
21 |
22 | def getmove(self, game: draughts.Board, maxtime: Union[int, float, None] = None, time: Union[int, float, None] = None,
23 | increment: Union[int, float, None] = None, movetime: Union[int, float, None] = None
24 | ) -> Tuple[Optional[str], bytes, Dict[str, Any], int]:
25 | """Send a getmove to the engine."""
26 | assert maxtime is not None or time is not None or movetime is not None
27 | response: Tuple[Optional[str], bytes, Dict[str, Any], int] = self.request32('getmove', game, maxtime, time,
28 | increment, movetime)
29 | return response
30 |
--------------------------------------------------------------------------------
/draughts/engines/checkerboard_extra/engine_server.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import draughts
3 | from draughts.engines.checkerboard_extra.get_checker_board import get_board, from_board
4 | from typing import Tuple, Optional, Union, Dict, Any
5 |
6 | from msl.loadlib import Server32
7 |
8 |
9 | class Engine32Server(Server32):
10 | """32 bit server to run old Checkerboard engines."""
11 |
12 | def __init__(self, host: str, port: int, **kwargs: Any) -> None:
13 | super(Engine32Server, self).__init__(kwargs["dll_name"], 'windll', host, port)
14 |
15 | def enginecommand(self, command: str) -> Tuple[bytes, int]:
16 | """Send an enginecommand to the engine."""
17 | output = ctypes.create_string_buffer(b'', 1024)
18 | result = self.lib.enginecommand(ctypes.create_string_buffer(bytes(command.encode('ascii')), 256), output)
19 | return output.value, result
20 |
21 | def getmove(self, game: draughts.Board, maxtime: Union[int, float, None] = None, time: Union[int, float, None] = None,
22 | increment: Union[int, float, None] = None, movetime: Union[int, float, None] = None
23 | ) -> Tuple[Optional[str], bytes, Dict[str, Any], int]:
24 | """Send a getmove to the engine."""
25 |
26 | # From CheckerBoard API:
27 | WHITE = 1
28 | BLACK = 2
29 |
30 | board = get_board(game)
31 |
32 | # Reversed color because red (black) starts first and not white in english checkers in Checkerboard.
33 | color = WHITE if game.turn == draughts.WHITE else BLACK
34 |
35 | info = 0
36 | moreinfo = 0
37 | if movetime:
38 | info = info | (1 << 1) # 2nd bit means the engine has to think for exactly maxtime seconds
39 | elif time is not None and increment is not None:
40 | if time / .01 > 2 ** 15 - 1 or increment / .01 > 2 ** 15 - 1:
41 | info = info | (1 << 3) # 0.1 seconds
42 | info = info | (1 << 4) # 0.1 seconds
43 | time = int(time / .1)
44 | increment = int(increment / .1)
45 | elif time / .001 > 2 ** 15 - 1 or increment / .001 > 2 ** 15 - 1:
46 | info = info | (1 << 3) # 0.01 seconds
47 | time = int(time / .01)
48 | increment = int(increment / .01)
49 | else:
50 | info = info | (1 << 4) # 0.001 seconds
51 | time = int(time / .001)
52 | increment = int(increment / .001)
53 | bin_time = bin(time)[2:].zfill(16)
54 | if len(bin_time) > 16:
55 | bin_time = '1' * 16
56 | bin_inc = bin(increment)[2:].zfill(16)
57 | if len(bin_inc) > 16:
58 | bin_inc = '1' * 16
59 | moreinfo = eval('0b' + bin_time + bin_inc)
60 |
61 | if movetime is not None:
62 | maxtime_double = ctypes.c_double(float(movetime))
63 | else:
64 | assert maxtime is not None
65 | maxtime_double = ctypes.c_double(float(maxtime))
66 | output = ctypes.create_string_buffer(b'', 1024)
67 | playnow = ctypes.c_int(0)
68 |
69 | class coor(ctypes.Structure):
70 | _fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)]
71 |
72 | class CBmove(ctypes.Structure):
73 | _fields_ = [("jumps", ctypes.c_int), ("newpiece", ctypes.c_int), ("oldpiece", ctypes.c_int), ("from", coor),
74 | ("to", coor), ("path", coor * 12), ("del", coor * 12), ("delpiece", ctypes.c_int * 12)]
75 |
76 | # self.lib.getmove.argtypes = [((ctypes.c_int * 8) * 8), ctypes.c_int, ctypes.c_double, (ctypes.c_char * 1024),
77 | # ctypes.POINTER(ctypes.c_int), ctypes.c_int, ctypes.c_int, ctypes.POINTER(CBmove)]
78 |
79 | cbmove = CBmove()
80 |
81 | result = self.lib.getmove(board, color, maxtime_double, output, ctypes.byref(playnow), info, moreinfo,
82 | ctypes.byref(cbmove))
83 |
84 | old_fen = game._game.get_fen()
85 | new_fen = from_board(board, game)
86 | our_pieces, opponents_pieces = (['w', 'W'], ['b', 'B']) if old_fen[0] == 'W' else (['b', 'B'], ['w', 'W'])
87 | captures = []
88 | start_pos, end_pos = None, None
89 | for index in range(1, len(old_fen)):
90 | if old_fen[index] in our_pieces and new_fen[index] == 'e':
91 | start_pos = index
92 | elif new_fen[index] in our_pieces and old_fen[index] == 'e':
93 | end_pos = index
94 | elif old_fen[index] in opponents_pieces and new_fen[index] == 'e':
95 | captures.append(index)
96 | hub_pos_move = None
97 | if start_pos and end_pos:
98 | hub_pos_move = game._game.make_len_2(start_pos) + game._game.make_len_2(end_pos) + game._game.sort_captures(
99 | captures)
100 |
101 | cbmove_output_2 = {}
102 | cbmove_output_2['jumps'] = cbmove.jumps
103 | cbmove_output_2['oldpiece'] = cbmove.oldpiece
104 | cbmove_output_2['newpiece'] = cbmove.newpiece
105 | cbmove_output_2['to'] = cbmove.to.x, cbmove.to.y
106 | cbmove.to = getattr(cbmove, 'from')
107 | cbmove_output_2['from'] = cbmove.to.x, cbmove.to.y
108 | cbmove_output_2['path'] = [(cbmove.path[i].x, cbmove.path[i].y) for i in range(12)]
109 | cbmove.path = getattr(cbmove, 'del')
110 | cbmove_output_2['del'] = [(cbmove.path[i].x, cbmove.path[i].y) for i in range(12)]
111 | cbmove_output_2['delpiece'] = [cbmove.delpiece[i] for i in range(12)]
112 | return hub_pos_move, output.value, cbmove_output_2, result
113 |
--------------------------------------------------------------------------------
/draughts/engines/checkerboard_extra/get_checker_board.py:
--------------------------------------------------------------------------------
1 | import ctypes
2 | import draughts
3 | from math import ceil
4 |
5 |
6 | # From CheckerBoard API:
7 | WHITE = 1
8 | BLACK = 2
9 | MAN = 4
10 | KING = 8
11 | FREE = 0
12 |
13 |
14 | def get_board(board: draughts.Board) -> ctypes.Array:
15 | """Get a CheckerBoard board (for use in CheckerBoard engines) from a Board() object."""
16 |
17 | one_column = (ctypes.c_int * 8)
18 | checkerboard_board = (one_column * 8)()
19 |
20 | white_starts = board.variant not in ['english']
21 | flip_column = board.variant not in ['english', 'italian']
22 |
23 | for loc in range(1, board._game.board.position_count + 1):
24 | row = ceil(loc / board._game.board.width) - 1 # From get_row_from_position
25 |
26 | # Because in english black starts
27 | if not white_starts:
28 | row = (board._game.board.height - 1) - row
29 |
30 | column = (loc - 1) % board._game.board.width # From get_column
31 |
32 | # Because:
33 | # 1. In italian the bottom-left square isn't playable, so in CheckerBoard the board is flipped vertically.
34 | # 2. In most variants the bottom-left square for the starting side (usually white) is in column a,
35 | # while in english black starts, so the bottom-left square for the starting side (black) is in row h.
36 | if flip_column:
37 | column = board._game.board.width - 1 - column
38 |
39 | if row % 2 == 0:
40 | column = (column + 1) * 2 - 2 # To account for the always empty white squares
41 | else:
42 | column = (column + 1) * 2 - 1 # To account for the always empty white squares
43 |
44 | number = FREE
45 | if loc in board._game.board.searcher.filled_positions:
46 | piece = board._game.board.searcher.get_piece_by_position(loc)
47 | # In Checkerboard black starts first, so the colors are reversed
48 | if piece.player == draughts.WHITE and not piece.king:
49 | number = (WHITE if white_starts else BLACK) + MAN
50 | elif piece.player == draughts.BLACK and not piece.king:
51 | number = (BLACK if white_starts else WHITE) + MAN
52 | elif piece.player == draughts.WHITE and piece.king:
53 | number = (WHITE if white_starts else BLACK) + KING
54 | elif piece.player == draughts.BLACK and piece.king:
55 | number = (BLACK if white_starts else WHITE) + KING
56 | checkerboard_board[column][row] = number
57 |
58 | return checkerboard_board
59 |
60 |
61 | def from_board(checker_board: ctypes.Array, old_board: draughts.Board) -> str:
62 | """Get the Hub fen from a CheckerBoard board."""
63 |
64 | # board_numbers = [[square for square in column] for column in checker_board]
65 | fen = 'B' if old_board.turn == draughts.WHITE else 'W' # switch turns
66 |
67 | white_starts = old_board.variant not in ['english']
68 | flip_column = old_board.variant not in ['english', 'italian']
69 |
70 | for loc in range(1, old_board._game.board.position_count + 1):
71 | row = ceil(loc / old_board._game.board.width) - 1 # From get_row_from_position
72 | if not white_starts:
73 | row = (old_board._game.board.height - 1) - row
74 | column = (loc - 1) % old_board._game.board.width # From get_column
75 |
76 | # Because:
77 | # 1. In italian the bottom-left square isn't playable, so in CheckerBoard the board is flipped vertically.
78 | # 2. In most variants the bottom-left square for the starting side (usually white) is in column a,
79 | # while in english black starts, so the bottom-left square for the starting side (black) is in row h.
80 | if flip_column:
81 | column = old_board._game.board.width - 1 - column
82 |
83 | if row % 2 == 0:
84 | column = (column + 1) * 2 - 2 # To account for the always empty white squares
85 | else:
86 | column = (column + 1) * 2 - 1 # To account for the always empty white squares
87 |
88 | # In Checkerboard black starts first, so the colors are reversed
89 | if checker_board[column][row] == FREE:
90 | fen += 'e'
91 | elif checker_board[column][row] == (BLACK if white_starts else WHITE) + MAN:
92 | fen += 'b'
93 | elif checker_board[column][row] == (WHITE if white_starts else BLACK) + MAN:
94 | fen += 'w'
95 | elif checker_board[column][row] == (BLACK if white_starts else WHITE) + KING:
96 | fen += 'B'
97 | elif checker_board[column][row] == (WHITE if white_starts else BLACK) + KING:
98 | fen += 'W'
99 | return fen
100 |
--------------------------------------------------------------------------------
/draughts/engines/dxp.py:
--------------------------------------------------------------------------------
1 | import draughts.engines.dxp_communication.dxp_communication as dxp_communication
2 | import draughts
3 | import draughts.engine
4 | import subprocess
5 | import os
6 | import signal
7 | import threading
8 | import time
9 | import logging
10 | from typing import Optional, Dict, Union, List, Any, Tuple
11 |
12 | logger = logging.getLogger("pydraughts")
13 |
14 |
15 | class DXPEngine:
16 | def __init__(self, command: Union[List[str], str, None] = None,
17 | options: Optional[Dict[str, Union[str, int, bool]]] = None, initial_time: int = 0,
18 | cwd: Optional[str] = None, ENGINE: int = 5) -> None:
19 | if options is None:
20 | options = {}
21 | self.initial_time = initial_time
22 | self.max_moves = 0
23 | self.ip = '127.0.0.1'
24 | self.port = '27531'
25 | self.command = command
26 | self.engine_opened = True # Whether the engine is already open or pydraughts should open it
27 | self.wait_to_open_time = 10
28 | self.ENGINE = ENGINE
29 | self.info: Dict[str, Any] = {}
30 | self.id: Dict[str, str] = {}
31 | self.sender = dxp_communication.Sender()
32 | self.receiver = self.sender.receiver
33 | self.game_started = False
34 | self.exit = False
35 |
36 | self.configure(options)
37 |
38 | if not self.engine_opened:
39 | cwd = cwd or os.getcwd()
40 | cwd = os.path.realpath(os.path.expanduser(cwd))
41 | if type(command) == str:
42 | command = [command]
43 | command = list(filter(None, command))
44 | command[0] = os.path.realpath(os.path.expanduser(command[0]))
45 | command[0] = '"' + command[0] + '"'
46 | command = ' '.join(command)
47 | self.command = command
48 | self.p = self._open_process(command, cwd)
49 | self.engine_receive_thread = threading.Thread(target=self._recv)
50 | self.engine_receive_thread.start()
51 |
52 | self.start_time = time.perf_counter_ns()
53 | self.quit_time = 0
54 |
55 | def setoption(self, name: str, value: Union[str, int, bool]) -> None:
56 | """Set a DXP option."""
57 | if name == 'engine-opened':
58 | self.engine_opened = bool(value)
59 | elif name == 'ip':
60 | self.ip = str(value)
61 | elif name == 'port':
62 | self.port = str(value)
63 | elif name == 'wait-to-open-time':
64 | self.wait_to_open_time = int(value)
65 | elif name == 'max-moves':
66 | self.max_moves = int(value)
67 | elif name == 'initial-time':
68 | self.initial_time = int(value)
69 |
70 | def configure(self, options: Dict[str, Union[str, int, bool]]) -> None:
71 | """Configure many options at once."""
72 | for name, value in options.items():
73 | self.setoption(name, value)
74 |
75 | def _open_process(self, command: str, cwd: Optional[str] = None, shell: bool = True,
76 | _popen_lock: Any = threading.Lock()) -> subprocess.Popen:
77 | """Open the engine process."""
78 | kwargs = {
79 | "shell": shell,
80 | "stdout": subprocess.PIPE,
81 | "stderr": subprocess.STDOUT,
82 | "stdin": subprocess.PIPE,
83 | "bufsize": 1, # Line buffered
84 | "universal_newlines": True,
85 | }
86 | logger.debug(f'command: {command}, cwd: {cwd}')
87 |
88 | if cwd is not None:
89 | kwargs["cwd"] = cwd
90 |
91 | # Prevent signal propagation from parent process
92 | try:
93 | # Windows
94 | kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
95 | except AttributeError:
96 | # Unix
97 | kwargs["preexec_fn"] = os.setpgrp
98 |
99 | with _popen_lock: # Work around Python 2 Popen race condition
100 | return subprocess.Popen(command, **kwargs)
101 |
102 | def kill_process(self) -> None:
103 | """Kill the engine process."""
104 | if not self.engine_opened:
105 | wait_time = self.quit_time / 1e9 + 10 - time.perf_counter_ns() / 1e9
106 | logger.debug(f'wait time before killing: {wait_time}')
107 | if wait_time > 0:
108 | time.sleep(wait_time)
109 | self.exit = True
110 | try:
111 | # Windows
112 | logger.debug("Killing Windows.")
113 | self.p.send_signal(signal.CTRL_BREAK_EVENT)
114 | except AttributeError:
115 | # Unix
116 | logger.debug("Killing UNIX.")
117 | os.killpg(self.p.pid, signal.SIGTERM)
118 | time.sleep(7)
119 | os.killpg(self.p.pid, signal.SIGKILL)
120 |
121 | self.p.communicate()
122 | self.engine_receive_thread.join()
123 |
124 | def _connect(self) -> None:
125 | """Connect to the engine."""
126 | if not self.engine_opened:
127 | wait_time = self.start_time / 1e9 + self.wait_to_open_time - time.perf_counter_ns() / 1e9
128 | logger.debug(f'wait time before connecting: {wait_time}')
129 | if wait_time > 0:
130 | time.sleep(wait_time)
131 | self.sender.connect(self.ip, int(self.port))
132 |
133 | def _start(self, board: draughts.Board, game_time: int) -> None:
134 | """Start the game."""
135 | self._connect()
136 | engine_color = 'B' if board.turn == draughts.WHITE else 'W'
137 | game_time = int(round(game_time // 60))
138 | moves = self.max_moves
139 | self.sender.setup(board.initial_fen, board.variant)
140 | self.sender.gamereq(engine_color, game_time, moves)
141 | accepted = self._recv_accept()
142 | logger.debug(f'Aceepted: {accepted}')
143 | self.id["name"] = self.sender.current.engineName
144 |
145 | def _recv(self) -> None:
146 | """Receive a line from the engine, if the engine is opened by pydraughts."""
147 | # The engine doesn't work otherwise.
148 | while True:
149 | try:
150 | line = self.p.stdout.readline()
151 |
152 | line = line.rstrip()
153 |
154 | if line:
155 | logger.debug(f"{self.ENGINE} %s >> %s {self.p.pid} {line}")
156 | except ValueError as err:
157 | if self.exit:
158 | break
159 | else:
160 | raise err
161 |
162 | def _recv_accept(self) -> bool:
163 | """Get if the game was accepted."""
164 | while True:
165 | if self.receiver.accepted is not None:
166 | return self.receiver.accepted
167 |
168 | def _recv_move(self) -> Optional[draughts.Move]:
169 | """Receive the engine move."""
170 | while True:
171 | if not self.receiver.listening:
172 | break
173 | if self.receiver.last_move_changed:
174 | logger.debug(f'new last move: {self.receiver.last_move.board_move}')
175 | return self.receiver.last_move
176 | return None
177 |
178 | def _recv_backreq(self) -> bool:
179 | """Get if the backreq was accepted."""
180 | while True:
181 | if self.receiver.backreq_accepted is not None:
182 | return self.receiver.backreq_accepted
183 |
184 | def takeback(self, move: int, color: int) -> Tuple[bool, draughts.Board, Any]:
185 | """
186 | Attempt to take back moves.
187 | Warning: If it is the engine's turn to play in the new position, it will attempt to move.
188 | """
189 | self.receiver.backreq_accepted = None
190 | ply = (move - 1) * 2 + (0 if color == draughts.WHITE else 1)
191 | remove_count = 0
192 | best_move = None
193 | moves = list(map(lambda old_move: old_move.steps_move, self.sender.current.pos.move_stack))
194 | logger.debug(f"Move stack before removing: {moves}")
195 | self.sender.backreq(move, color)
196 | backreq = self._recv_backreq()
197 | if backreq:
198 | while len(self.sender.current.pos.move_stack) > ply:
199 | remove_count += 1
200 | self.sender.current.pos.pop()
201 | moves = list(map(lambda old_move: old_move.steps_move, self.sender.current.pos.move_stack))
202 | logger.debug(f"Move stack after removing {remove_count} moves: {moves}")
203 | self.receiver.takeback_in_progress = False
204 | new_board = self.sender.current.pos.copy()
205 | if self.sender.current.engine_color == self.sender.current.get_color(): # It is the engine's turn to play.
206 | best_move = self._recv_move()
207 | else:
208 | new_board = self.sender.current.pos.copy()
209 | return backreq, new_board, draughts.engine.PlayResult(best_move, None, {})
210 |
211 | def play(self, board: draughts.Board) -> Any:
212 | """Engine search."""
213 | if not self.game_started:
214 | self._start(board, self.initial_time)
215 | self.game_started = True
216 | if board.move_stack:
217 | self.sender.send_move(board.move_stack[-1])
218 | best_move = self._recv_move()
219 | return draughts.engine.PlayResult(best_move, None, {})
220 |
221 | def quit(self) -> None:
222 | """Quit the engine."""
223 | self.sender.gameend()
224 | self.sender.disconnect()
225 | self.quit_time = time.perf_counter_ns()
226 |
--------------------------------------------------------------------------------
/draughts/engines/dxp_communication/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/draughts/engines/dxp_communication/__init__.py
--------------------------------------------------------------------------------
/draughts/engines/dxp_communication/dxp_classes.py:
--------------------------------------------------------------------------------
1 | # This file is an adaptation of DXC100_draughts_client (https://github.com/akalverboer/DXC100_draughts_client) by akalverboer.
2 |
3 | from __future__ import annotations
4 | import socket
5 | import logging
6 | import draughts
7 | import time
8 | from typing import Dict, Optional, List, Union
9 |
10 | logger = logging.getLogger("pydraughts")
11 |
12 | DXP_WHITE = 0
13 | DXP_BLACK = 1
14 |
15 |
16 | class GameStatus:
17 | def __init__(self, fen: str = 'startpos', engine_color: int = DXP_WHITE, started: bool = False,
18 | variant: str = 'standard') -> None:
19 | self.fen = fen
20 | self.engine_color = engine_color
21 | self.started = started
22 | self.variant = variant
23 | self.pos = draughts.Board(fen=fen, variant=variant)
24 | self.color = self.get_color()
25 | self.engineName = ''
26 | self.result = None
27 |
28 | def get_color(self) -> int:
29 | """Get the color of the playing side."""
30 | return DXP_WHITE if self.pos.turn == draughts.WHITE else DXP_BLACK
31 |
32 |
33 | class MySocket:
34 | def __init__(self) -> None:
35 | self.sock: Optional[socket.socket] = None
36 | self.closed = False
37 |
38 | def open(self) -> MySocket:
39 | """Open the socket."""
40 | try:
41 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
42 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
43 | self.closed = False
44 | except Exception:
45 | self.sock = None
46 | raise Exception("socket exception: failed to open")
47 | return self
48 |
49 | def connect(self, host: str, port: int) -> MySocket:
50 | """Connect to the engine."""
51 | self.sock.settimeout(10) # timeout for connection
52 | try:
53 | self.sock.connect((host, port))
54 | except socket.error as msg:
55 | self.sock = None
56 | raise Exception(f"connection exception: failed to connect ({msg}).")
57 | if self.sock is not None:
58 | self.sock.settimeout(None) # default
59 | return self
60 |
61 | def send(self, msg: str) -> None:
62 | """Send a message to the engine."""
63 | try:
64 | logger.debug(f"socket send: {msg}")
65 | self.sock.send(bytes(msg, 'utf-8') + b"\0")
66 | except Exception:
67 | raise Exception("send exception: no connection")
68 | return None
69 |
70 | def receive(self) -> str:
71 | """Receive a message from the engine."""
72 | msg = ""
73 | while True:
74 | # Collect message chunks until null character found
75 | try:
76 | chunk = self.sock.recv(1024)
77 | except Exception:
78 | raise Exception("receive exception: no connection")
79 |
80 | if chunk == "":
81 | raise Exception("receive exception: socket connection broken")
82 | msg += chunk.decode()
83 | if msg.find("\0") > -1:
84 | break
85 | if len(msg) > 128:
86 | break # too long, no null char
87 |
88 | logger.debug(f"socket receive: {msg}")
89 | msg = msg.replace("\0", "") # remove all null chars
90 |
91 | # Use strip to remove all whitespace at the start and end.
92 | # Including spaces, tabs, newlines and carriage returns.
93 | msg = msg.strip()
94 | return msg
95 |
96 | def close(self) -> None:
97 | if self.sock and not self.closed:
98 | self.closed = True
99 | self.sock.shutdown(socket.SHUT_RDWR)
100 | time.sleep(7)
101 | self.sock.close()
102 | self.sock = None
103 |
104 | def __del__(self) -> None:
105 | self.close()
106 |
107 |
108 | class DamExchange:
109 | def parse(self, msg: str) -> Dict[str, Union[str, List[str]]]:
110 | """Parse an incoming DXP message."""
111 | # Parse incoming DXP message. Returns relevant items depending on mtype.
112 | result: Dict[str, Union[str, List[str]]] = {}
113 | mtype = msg[0:1]
114 | if mtype == "C": # CHAT
115 | result['type'] = "C"
116 | result['text'] = msg[1:127]
117 | elif mtype == "R": # GAMEREQ (only received by FOLLOWER)
118 | result['type'] = "R"
119 | result['name'] = msg[3:35].strip() # initiator
120 | result['fColor'] = msg[35:36] # color of follower
121 | result['gameTime'] = msg[36:39]
122 | result['numMoves'] = msg[39:42]
123 | result['posInd'] = msg[42:43]
124 | if result['posInd'] != "A":
125 | result['mColor'] = msg[43:44] # color to move for position
126 | result['pos'] = msg[44:94]
127 | elif mtype == "A": # GAMEACC
128 | result['type'] = "A"
129 | result['engineName'] = msg[1:33].strip() # follower name
130 | result['accCode'] = msg[33:34]
131 | elif mtype == "M": # MOVE
132 | result['type'] = "M"
133 | result['time'] = msg[1:5]
134 | result['from'] = msg[5:7]
135 | result['to'] = msg[7:9]
136 | result['nCaptured'] = msg[9:11]
137 | result['captures'] = []
138 | for i in range(int(result['nCaptured'])):
139 | s = i * 2
140 | result['captures'].append(msg[11 + s:13 + s])
141 | elif mtype == "E": # GAMEEND
142 | result['type'] = "E"
143 | result['reason'] = msg[1:2]
144 | result['stop'] = msg[2:3]
145 | elif mtype == "B": # BACKREQ
146 | result['type'] = "B"
147 | result['moveId'] = msg[1:4]
148 | result['mColor'] = msg[4:5]
149 | elif mtype == "K": # BACKACC
150 | result['type'] = "K"
151 | result['accCode'] = msg[1:2]
152 | else:
153 | result['type'] = "?"
154 | return result
155 |
156 | def msg_chat(self, msg: str) -> str:
157 | """Generate a CHAT message."""
158 | # Generate CHAT message. Example: CWhat do you think about move 35?
159 | msg = "C" + msg
160 | return msg
161 |
162 | def msg_gamereq(self, my_color: int, game_time: int, num_moves: int, pos: Optional[draughts.Board] = None,
163 | color_to_move: Optional[int] = None) -> str:
164 | """Generate a GAMEREQ message."""
165 | # Generate GAMEREQ message. Example: R01Tornado voor Windows 4.0 W060065A
166 | gamereq = []
167 | gamereq.append("R") # header
168 | gamereq.append("01") # version
169 |
170 | gamereq.append("DXP Client".ljust(32)[:32]) # iName: fixed length padding spaces
171 | gamereq.append('Z' if my_color == DXP_WHITE else 'W') # fColor: color of follower (server)
172 | gamereq.append(str(game_time).zfill(3)) # game_time: time limit of game (ex: 090)
173 | gamereq.append(str(num_moves).zfill(3)) # num_moves: number of moves of time limit (ex: 050)
174 | if pos is None or color_to_move is None:
175 | gamereq.append("A") # posInd == A: use starting position
176 | else:
177 | gamereq.append("B") # posInd == B: use parameters pos and color_to_move
178 | gamereq.append("W" if color_to_move == DXP_WHITE else "Z") # mColor
179 | gamereq.append(pos._game.get_dxp_fen()) # board
180 |
181 | msg = ""
182 | for item in gamereq:
183 | msg = msg + item
184 | return msg
185 |
186 | def msg_move(self, steps: List[int], captures: List[int], time_spent: int) -> str:
187 | """Generate a MOVE message."""
188 | # Generate MOVE message. Example: M001205250422122320
189 | # Parm rmove is a "two-color" move
190 | move = []
191 | move.append("M") # header
192 | move.append(str(time_spent % 10000).zfill(4)) # mTime: 0000 .. 9999
193 | move.append(str(steps[0] % 100).zfill(2)) # mFrom
194 | move.append(str(steps[-1] % 100).zfill(2)) # mTo
195 | move.append(str(len(captures) % 100).zfill(2)) # mNumCaptured: number of takes (captures)
196 | for k in captures:
197 | move.append(str(k % 100).zfill(2)) # mCaptures
198 |
199 | msg = ""
200 | for item in move:
201 | msg = msg + item
202 | return msg
203 |
204 | def msg_gameend(self, reason: str) -> str:
205 | """Generate a GAMEEND message."""
206 | # Generate GAMEEND message. Example: E00
207 | gameend = []
208 | gameend.append("E") # header
209 | gameend.append(str(reason)[0]) # reason: 0 > unknown 1 > I lose 2 > draw 3 > I win
210 | gameend.append("1") # stop code: 0 > next game preferred 1: > no next game
211 | msg = ""
212 | for item in gameend:
213 | msg = msg + item
214 | return msg
215 |
216 | def msg_backreq(self, moveId: int, color_to_move: int) -> str:
217 | """Generate a BACKREQ message."""
218 | # Generate BACKREQ message. Example: B005Z
219 | backreq = []
220 | backreq.append("B")
221 | backreq.append(str(moveId % 1000).zfill(3)) # moveId
222 | backreq.append("W" if color_to_move == DXP_WHITE else "Z") # mColor
223 | msg = ""
224 | for item in backreq:
225 | msg = msg + item
226 | return msg
227 |
228 | def msg_backacc(self, accCode: str) -> str:
229 | """Generate the response to a BACKREQ request."""
230 | # Generate BACKREQ message. Example: K1
231 | backreq = []
232 | backreq.append("K")
233 | backreq.append(str(accCode[0])) # accCode
234 | msg = ""
235 | for item in backreq:
236 | msg = msg + item
237 | return msg
238 |
--------------------------------------------------------------------------------
/draughts/engines/dxp_communication/dxp_communication.py:
--------------------------------------------------------------------------------
1 | # This file is an adaptation of DXC100_draughts_client (https://github.com/akalverboer/DXC100_draughts_client) by akalverboer.
2 |
3 | from __future__ import annotations
4 | import threading
5 | import logging
6 | from draughts.engines.dxp_communication.dxp_classes import DamExchange, MySocket, GameStatus, DXP_WHITE, DXP_BLACK
7 | from draughts import Move, WHITE
8 | from typing import Optional
9 |
10 | logger = logging.getLogger("pydraughts")
11 |
12 |
13 | class Receiver:
14 | def __init__(self, sender: Sender) -> None:
15 | self.sender = sender
16 | self.accepted: Optional[bool] = None
17 | self.gameend_sent = False
18 | self.last_move: Optional[Move] = None
19 | self.last_move_changed = False
20 | self.listening = False
21 | self.receive_thread_started = False
22 | self.backreq_accepted: Optional[bool] = None
23 | self.takeback_in_progress = False
24 | self.receive_thread = threading.Thread(target=self.receive)
25 |
26 | def start(self) -> None:
27 | self.receive_thread_started = True
28 | self.receive_thread.start()
29 |
30 | def close(self) -> None:
31 | if self.receive_thread_started:
32 | self.receive_thread.join()
33 |
34 | def receive(self) -> None:
35 | logger.debug("DXP Client starts listening")
36 | self.listening = True
37 | while True:
38 | try:
39 | message = self.sender.socket.receive() # wait for message
40 | except Exception as err:
41 | logger.debug(f"Error {err}")
42 | self.sender.socket.close()
43 | break
44 |
45 | message = message[0:127] # DXP max length
46 | dxp_data = self.sender.dxp.parse(message)
47 | if dxp_data["type"] == "C":
48 | logger.debug(f"rcv CHAT: {message}")
49 | logger.debug(f"\nChat message: {dxp_data['text']}")
50 |
51 | elif dxp_data["type"] == "A":
52 | logger.debug(f"rcv GAMEACC: {message}")
53 | if dxp_data["accCode"] == "0":
54 | self.sender.current.engine_color = self.sender.current.engine_color # as requested
55 | self.sender.current.engineName = dxp_data["engineName"]
56 | logger.debug(f"\nGame request accepted by {dxp_data['engineName']}")
57 | self.accepted = True
58 | self.gameend_sent = False
59 | self.sender.current.started = True
60 | else:
61 | logger.debug(f"\nGame request NOT accepted by {dxp_data['engineName']} Reason: {dxp_data['accCode']}")
62 | self.accepted = False
63 | self.sender.current.started = False
64 |
65 | elif dxp_data["type"] == "E":
66 | logger.debug(f"rcv GAMEEND: {message}")
67 | logger.debug(f"\nRequest end of game accepted. Reason: {dxp_data['reason']} Stop: {dxp_data['stop']}")
68 | # Confirm game end by sending message back (if not sent by me)
69 | if self.sender.current.started and not self.gameend_sent:
70 | self.sender.current.result = dxp_data["reason"]
71 | msg = self.sender.dxp.msg_gameend(str(dxp_data["reason"])) # `str` is there only for mypy.
72 | self.sender.socket.send(msg)
73 | logger.debug(f"snd GAMEEND: {msg}")
74 | self.gameend_sent = True
75 | self.sender.current.started = False
76 |
77 | elif dxp_data["type"] == "M":
78 | logger.debug(f"rcv MOVE: {message}")
79 | steps = [dxp_data['from'], dxp_data['to']]
80 | nsteps = list(map(int, steps))
81 | ntakes_int = list(map(int, dxp_data['captures']))
82 | ntakes = list(map(lambda pos: str(pos).zfill(2), sorted(ntakes_int)))
83 | logger.debug(f"FEN: {self.sender.current.pos.fen}, Steps: {nsteps}, Takes: {ntakes}")
84 | correct_move = None
85 | while True:
86 | if not self.takeback_in_progress:
87 | break
88 | for move in self.sender.current.pos.legal_moves():
89 | if move.hub_position_move == f"{str(nsteps[0]).zfill(2)}{str(nsteps[-1]).zfill(2)}{''.join(ntakes)}":
90 | correct_move = move
91 |
92 | if correct_move is not None:
93 | self.sender.current.pos.push(correct_move)
94 | self.last_move = correct_move
95 | logger.debug(f"Move received: {correct_move.steps_move}")
96 | self.last_move_changed = True
97 | else:
98 | move_history = list(map(lambda old_move: old_move.steps_move, self.sender.current.pos.move_stack))
99 | logger.debug(f"Error: received move is illegal [{message}]\nMove history: {move_history}")
100 |
101 | elif dxp_data["type"] == "B":
102 | # For the time being do not confirm request from server: send message back.
103 | logger.debug(f"rcv BACKREQ: {message}")
104 | acc_code = "1" # 0: BACK YES; 1: BACK NO; 2: CONTINUE
105 | msg = self.sender.dxp.msg_backacc(acc_code)
106 | self.sender.socket.send(msg)
107 | logger.debug(f"snd BACKACC: {msg}")
108 |
109 | elif dxp_data["type"] == "K":
110 | # Answer to my request to move back
111 | logger.debug(f"rcv BACKACC: {message}")
112 | logger.debug(f"rcv BACKACC: {message}") # TEST
113 | acc_code = dxp_data['accCode']
114 | if acc_code == "0":
115 | # Actions to go back in history as specified in my request
116 | self.backreq_accepted = True
117 | elif acc_code == "1":
118 | logger.debug("Engine doesn't support going back.")
119 | self.backreq_accepted = False
120 | else:
121 | logger.debug("Engine wants to continue the game.")
122 | self.backreq_accepted = False
123 |
124 | else:
125 | logger.debug(f"rcv UNKNOWN: {message}")
126 | logger.debug(f"\nrcv Unknown message: {message}")
127 |
128 | self.listening = False
129 |
130 |
131 | class Sender:
132 | def __init__(self) -> None:
133 | self.dxp = DamExchange()
134 | self.socket = MySocket()
135 | self.current = GameStatus("W")
136 | self.receiver = Receiver(self)
137 |
138 | def setup(self, fen: str, variant: str) -> None:
139 | logger.debug(f"FEN: {fen.strip()}, Variant: {variant}")
140 | engine_color = DXP_BLACK if fen[0] == 'B' else DXP_WHITE
141 | self.current = GameStatus(fen=fen, engine_color=engine_color, variant=variant)
142 |
143 | def send_move(self, move: Move) -> None:
144 | logger.debug(f"FEN: {self.current.pos.fen}, Steps: {move.steps_move}, Captures: {move.captures}")
145 | time_spent = 0
146 | self.receiver.last_move_changed = False
147 | self.current.pos.push(move)
148 | msg = self.dxp.msg_move(move.steps_move, move.captures, time_spent)
149 | try:
150 | self.socket.send(msg)
151 | logger.debug(f"snd MOVE: {msg}")
152 | except Exception as err:
153 | logger.debug(f"Error sending move: {err}")
154 | return
155 |
156 | def connect(self, host: str, port: int) -> None:
157 | logger.debug(f"Host: {host}, Port: {port}")
158 | try:
159 | self.socket.open()
160 | self.socket.connect(host, int(port)) # with timeout
161 | logger.debug(f"Successfully connected to remote host {host}, port {port}")
162 | except Exception as err:
163 | self.socket.sock = None
164 | logger.debug(f"Error trying to connect: {err}")
165 | return
166 | self.receiver.start()
167 |
168 | def disconnect(self) -> None:
169 | logger.debug("Attempting to disconnect.")
170 | try:
171 | self.socket.close()
172 | logger.debug("Successfully closed socket.")
173 | except Exception as err:
174 | self.socket.sock = None
175 | logger.debug(f"Error trying to close socket: {err}")
176 | self.receiver.close()
177 |
178 | def chat(self, text: str) -> None:
179 | text = text.strip() # trim whitespace
180 | logger.debug(f"Text: {text}")
181 | msg = self.dxp.msg_chat(text)
182 | try:
183 | self.socket.send(msg)
184 | logger.debug(f"snd CHAT: {msg}")
185 | except Exception as err:
186 | logger.debug(f"Error sending chat message: {err}")
187 |
188 | def gamereq(self, engine_color: str, time: int, max_moves: int) -> None:
189 | logger.debug(f"Engine color: {engine_color}, Time: {time}, Max moves: {max_moves}")
190 | dxp_color = DXP_WHITE if engine_color.upper().startswith('W') else DXP_BLACK
191 | msg = self.dxp.msg_gamereq(dxp_color, time, max_moves, self.current.pos, self.current.get_color())
192 | try:
193 | self.socket.send(msg)
194 | logger.debug(f"snd GAMEREQ: {msg}")
195 | except Exception as err:
196 | logger.debug(f"Error sending game request: {err}")
197 |
198 | def gameend(self) -> None:
199 | logger.debug("Attempting to send a gameend.")
200 | reason = "0"
201 | msg = self.dxp.msg_gameend(reason)
202 | self.receiver.gameend_sent = True
203 | try:
204 | self.socket.send(msg)
205 | logger.debug(f"snd GAMEEND: {msg}")
206 | except Exception as err:
207 | logger.debug(f"Error sending gameend message: {err}")
208 |
209 | def backreq(self, move: int, color: int) -> None:
210 | logger.debug(f"Request to return to {move}th move with {'WHITE' if color == WHITE else 'BLACK'} to move.")
211 | msg = self.dxp.msg_backreq(move, DXP_WHITE if color == WHITE else DXP_BLACK)
212 | self.receiver.last_move_changed = False
213 | self.receiver.takeback_in_progress = True
214 | try:
215 | self.socket.send(msg)
216 | logger.debug(f"snd BACKREQ: {msg}")
217 | except Exception as err:
218 | logger.debug(f"Error sending backreq request: {err}")
219 |
--------------------------------------------------------------------------------
/draughts/engines/hub.py:
--------------------------------------------------------------------------------
1 | # This file is an adaptation of fishnet (https://github.com/lichess-org/fishnet/tree/ebd2a5e16d37135509cbfbff9998e0b798866ef5).
2 |
3 | import subprocess
4 | import os
5 | import signal
6 | import logging
7 | import threading
8 | import draughts
9 | import draughts.engine
10 | import re
11 | import math
12 | from typing import Union, Optional, List, Tuple, Any, Dict, Set
13 |
14 | logger = logging.getLogger("pydraughts")
15 |
16 |
17 | class HubEngine:
18 | def __init__(self, command: Union[List[str], str], cwd: Optional[str] = None, ENGINE: int = 5) -> None:
19 | self.ENGINE = ENGINE
20 | self.info: Dict[str, Any] = {}
21 | self.id: Dict[str, str] = {}
22 | self.options: Set[str] = set()
23 | self.variants: Set[str] = set()
24 | cwd = cwd or os.getcwd()
25 | cwd = os.path.realpath(os.path.expanduser(cwd))
26 | if type(command) == str:
27 | command = [command]
28 | command = list(filter(bool, command))
29 | command[0] = os.path.realpath(os.path.expanduser(command[0]))
30 | command[0] = '"' + command[0] + '"'
31 | command = ' '.join(command)
32 | self.p = self._open_process(command, cwd)
33 | self._last_sent = ""
34 | self.hub()
35 |
36 | def _open_process(self, command: str, cwd: Optional[str] = None, shell: Optional[bool] = True,
37 | _popen_lock: Any = threading.Lock()) -> subprocess.Popen:
38 | """Open the engine process."""
39 | kwargs: Dict[str, Any] = {
40 | "shell": shell,
41 | "stdout": subprocess.PIPE,
42 | "stderr": subprocess.STDOUT,
43 | "stdin": subprocess.PIPE,
44 | "bufsize": 1, # Line buffered
45 | "universal_newlines": True,
46 | }
47 |
48 | if cwd is not None:
49 | kwargs["cwd"] = cwd
50 |
51 | # Prevent signal propagation from parent process
52 | try:
53 | # Windows
54 | kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
55 | except AttributeError:
56 | # Unix
57 | kwargs["preexec_fn"] = os.setpgrp
58 |
59 | with _popen_lock: # Work around Python 2 Popen race condition
60 | return subprocess.Popen(command, **kwargs)
61 |
62 | def kill_process(self) -> None:
63 | """Kill the engine process."""
64 | try:
65 | # Windows
66 | self.p.send_signal(signal.CTRL_BREAK_EVENT)
67 | except AttributeError:
68 | # Unix
69 | os.killpg(self.p.pid, signal.SIGKILL)
70 |
71 | self.p.communicate()
72 |
73 | def send(self, line: str) -> None:
74 | """Send a command to the engine."""
75 | if line == "ponder-hit":
76 | while self._last_sent != "go ponder":
77 | pass
78 | logger.debug(f"{self.ENGINE} %s << %s {self.p.pid} {line}")
79 |
80 | self.p.stdin.write(line + "\n")
81 | self.p.stdin.flush()
82 | self._last_sent = line
83 |
84 | def recv(self) -> str:
85 | """Receive a line from the engine."""
86 | while True:
87 | line = self.p.stdout.readline()
88 | if line == "":
89 | raise EOFError()
90 |
91 | line = line.rstrip()
92 |
93 | logger.debug(f"{self.ENGINE} %s >> %s {self.p.pid} {line}")
94 |
95 | if line:
96 | return line
97 |
98 | def recv_hub(self) -> List[str]:
99 | """Receive a line from the engine and split at the first space."""
100 | command_and_args = self.recv().split(None, 1)
101 | if len(command_and_args) == 1:
102 | return [command_and_args[0], ""]
103 | elif len(command_and_args) == 2:
104 | return command_and_args
105 | return []
106 |
107 | def hub(self) -> Tuple[Dict[str, str], Set[str], Set[str]]:
108 | """Send the hub command to an engine."""
109 | self.send("hub")
110 |
111 | engine_info: Dict[str, str] = {}
112 | options: Set[str] = set()
113 | variants: Set[str] = set()
114 |
115 | while True:
116 | command, arg = self.recv_hub()
117 |
118 | if command == "wait":
119 | return engine_info, options, variants
120 | elif command == "id":
121 | args = re.split(r' +(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)', arg)
122 | split_args = list(map(lambda item: item.split("="), args))
123 | for key, value in split_args:
124 | engine_info[key] = value
125 | elif command == "param":
126 | args = re.split(r' +(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)', arg)
127 | split_args = list(map(lambda item: item.split("="), args))
128 | is_variant = False
129 | for key, value in split_args:
130 | if key == "name":
131 | options.add(value)
132 | if value == "variant":
133 | is_variant = True
134 | if key == "values" and is_variant:
135 | if value.startswith('"') and value.endswith('"'):
136 | value = value[1:-1]
137 | for variant in value.split():
138 | variants.add(variant)
139 | else:
140 | logger.warning("Unexpected engine response to hub: %s %s", command, arg)
141 | self.options = options
142 | self.variants = variants
143 | self.id = engine_info
144 |
145 | def init(self) -> None:
146 | """Initialize the engine."""
147 | self.send("init")
148 | while True:
149 | command, arg = self.recv_hub()
150 | if command == "ready":
151 | break
152 | elif command == "init":
153 | pass
154 | else:
155 | logger.warning("Unexpected engine response to init: %s %s", command, arg)
156 |
157 | def ping(self) -> None:
158 | """Send the engine ping. They should reply with pong."""
159 | self.send("ping")
160 | while True:
161 | command, arg = self.recv_hub()
162 | if command == "pong":
163 | break
164 | else:
165 | logger.warning("Unexpected engine response to ping: %s %s", command, arg)
166 |
167 | def setoption(self, name: str, value: Union[str, bool, None]) -> None:
168 | """Set an engine option."""
169 | if value is True:
170 | value = "true"
171 | elif value is False:
172 | value = "false"
173 | elif value is None:
174 | value = "none"
175 |
176 | if name == 'variant' and self.variants or name != 'variant':
177 | self.send("set-param name=%s value=%s" % (name, value))
178 |
179 | def configure(self, options: Dict[str, Union[str, bool, None]]) -> None:
180 | """Configure many options at once."""
181 | for name, value in options.items():
182 | self.setoption(name, value)
183 |
184 | def go(self, fen: str, moves: Optional[str] = None, my_time: Union[int, float, None] = None,
185 | inc: Union[int, float, None] = None, moves_left: Optional[int] = None,
186 | movetime: Union[int, float, None] = None, depth: Optional[int] = None, nodes: Optional[int] = None,
187 | ponder: Optional[bool] = False) -> Tuple[str, Optional[str]]:
188 | """Send the engine a go command."""
189 | if moves:
190 | self.send(f'pos pos={fen} moves="{moves}"')
191 | else:
192 | self.send(f'pos pos={fen}')
193 |
194 | if my_time is not None and inc and moves_left:
195 | my_time -= inc # Hub engines first add the increment
196 | self.send(f'level moves={moves_left} time={my_time} inc={inc}')
197 | elif my_time is not None and inc:
198 | my_time -= inc # Hub engines first add the increment
199 | self.send(f'level time={my_time} inc={inc}')
200 | elif my_time is not None and moves_left:
201 | self.send(f'level moves={moves_left} time={my_time}')
202 | elif my_time is not None:
203 | self.send(f'level time={my_time}')
204 | elif movetime is not None:
205 | self.send(f'level move-time={movetime}')
206 | elif depth is not None:
207 | self.send(f'level depth={depth}')
208 | elif nodes is not None:
209 | self.send(f'level nodes={nodes}')
210 |
211 | if ponder:
212 | self.send('go ponder')
213 | else:
214 | self.send('go think')
215 |
216 | self.info = {}
217 | while True:
218 | command, arg = self.recv_hub()
219 | if command == "done":
220 | args = arg.split()
221 | pondermove = None
222 | split_args = list(map(lambda item: item.split("="), args))
223 | bestmove = split_args[0][1]
224 | if len(split_args) == 2:
225 | pondermove = split_args[1][1]
226 | return bestmove, pondermove
227 | elif command == "info":
228 | args = re.split(r' +(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)', arg)
229 | split_args = list(map(lambda item: item.split("="), args))
230 | value: Any
231 | for key, value in split_args:
232 | if key in ["depth", "nodes"]:
233 | value = int(value)
234 | elif key in ["mean-depth", "time", "nps"]:
235 | value = float(value)
236 | elif key == "score":
237 | score = int(float(value) * 100)
238 | mate = None
239 | if score > 9000:
240 | mate = 10000 - score
241 | elif score < -9000:
242 | mate = -10000 - score
243 | if mate:
244 | value = {"win": math.ceil(mate / 2)}
245 | else:
246 | value = {"cp": score}
247 | self.info[key] = value
248 | else:
249 | logger.warning("Unexpected engine response to go: %s %s", command, arg)
250 |
251 | def stop(self) -> None:
252 | """Stop the engine from searching."""
253 | self.send("stop")
254 |
255 | def ponderhit(self) -> None:
256 | """Send ponder-hit to the engine."""
257 | self.send("ponder-hit")
258 |
259 | def quit(self) -> None:
260 | """Quit the engine."""
261 | self.send("quit")
262 |
263 | def play(self, board: draughts.Board, time_limit: Any, ponder: bool) -> Any:
264 | """Engine search."""
265 | time = time_limit.time
266 | inc = time_limit.inc
267 | depth = time_limit.depth
268 | nodes = time_limit.nodes
269 | movetime = time_limit.movetime
270 | hub_moves = list(map(lambda move: move.hub_move, board.move_stack))
271 | bestmove, pondermove = self.go(board._game.initial_hub_fen, moves=' '.join(hub_moves), my_time=time, inc=inc,
272 | depth=depth, nodes=nodes, movetime=movetime, ponder=ponder)
273 |
274 | ponder_move = None
275 | ponder_board = board.copy()
276 | best_move = draughts.Move(ponder_board, hub_move=bestmove)
277 | if pondermove:
278 | ponder_board.push(best_move)
279 | ponder_move = draughts.Move(ponder_board, hub_move=pondermove)
280 |
281 | return draughts.engine.PlayResult(best_move, ponder_move, self.info)
282 |
--------------------------------------------------------------------------------
/draughts/svg.py:
--------------------------------------------------------------------------------
1 | import math
2 | from draughts import Board
3 |
4 |
5 | def create_svg(board: Board) -> str:
6 | """Create an SVG of a board."""
7 | # Base square size
8 | square_size = 40
9 | margin = 16 # Fixed margin size for coordinates
10 |
11 | # Calculate SVG dimensions based on board size
12 | str_representation = list(map(lambda row_str: row_str.split("|"), filter(lambda row_str: "|" in row_str, str(board).split("\n"))))
13 | width = len(str_representation[0])
14 | height = len(str_representation)
15 | svg_width = (square_size * width) + (2 * margin)
16 | svg_height = (square_size * height) + (2 * margin)
17 |
18 | # Background color for coordinates
19 | svg = [f'''')
94 | return '\n'.join(svg)
95 |
--------------------------------------------------------------------------------
/draughts/tournament.py:
--------------------------------------------------------------------------------
1 | from draughts.engine import HubEngine, DXPEngine, CheckerBoardEngine, Limit
2 | from draughts import Board, WHITE, BLACK
3 | from draughts.PDN import PDNWriter
4 | from typing import List, Tuple, Dict, Any, Union, Optional
5 | import datetime
6 | import itertools
7 | import time
8 | import logging
9 |
10 | logger = logging.getLogger("pydraughts")
11 |
12 |
13 | class RoundRobin:
14 | def __init__(self, filename: str, players: List[Tuple[Union[str, List[str]], str, Dict[str, Any], Optional[str]]],
15 | start_time: Union[int, float], increment: Union[int, float] = 0, variant: str = "standard",
16 | games_per_pair: int = 2, starting_fen: str = "startpos", max_moves: int = 300) -> None:
17 | self.filename = filename
18 | self.players = players
19 | self.start_time = start_time
20 | self.increment = increment
21 | self.variant = variant
22 | self.games_per_pair = games_per_pair
23 | self.starting_fen = starting_fen
24 | self.max_moves = max_moves
25 | self.player_count = len(self.players)
26 | self.int_players = list(range(self.player_count))
27 | self.results = [[0, 0, 0] for _ in range(self.player_count)]
28 | self.scores = [0] * self.player_count
29 | self.complete_results: List[List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]]] = []
30 | self.pairs: List[Tuple[int, int]] = list(itertools.combinations(self.int_players, 2))
31 | logger.debug(f"There are {len(self.pairs)} possible pairs.")
32 | self.complete_pairs: List[List[Tuple[int, int]]] = []
33 | self.get_complete_pairs()
34 | logger.debug(f"There will be {len(self.complete_pairs)} games.")
35 | date = datetime.datetime.now()
36 | year = str(date.year).zfill(4)
37 | month = str(date.month).zfill(2)
38 | day = str(date.day).zfill(2)
39 | self.tags = {"Event": "Tournament", "Site": "pydraughts", "EventDate": f"{year}.{month}.{day}",
40 | "ResultType": "International", "WhiteType": "program", "BlackType": "program", "GameType": "20",
41 | "TimeControl": f"{self.start_time}+{self.increment}"}
42 | if self.starting_fen != "startpos":
43 | self.tags["FEN"] = self.starting_fen
44 | self.round = 0
45 |
46 | self.latest_round_results: List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]] = []
47 | self.latest_complete_results: List[List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]]] = []
48 |
49 | def get_complete_pairs(self) -> None:
50 | pairs = self.pairs.copy()
51 | for match in range(self.games_per_pair):
52 | self.complete_pairs.append(pairs.copy())
53 | pairs = list(map(lambda _pair: (_pair[1], _pair[0]), pairs))
54 |
55 | def get_engine(self, command: Union[str, List[str]], protocol: str, options: Dict[str, Any], cwd: Optional[str],
56 | ) -> Union[HubEngine, DXPEngine, CheckerBoardEngine]:
57 | engine: Union[HubEngine, DXPEngine, CheckerBoardEngine]
58 | if protocol.lower() == "hub":
59 | engine = HubEngine(command, cwd=cwd)
60 | engine.configure(options)
61 | elif protocol.lower() == "dxp":
62 | options["initial-time"] = self.start_time
63 | options["max-moves"] = self.max_moves
64 | engine = DXPEngine(command, options, cwd=cwd)
65 | elif protocol.lower() == "cb" or protocol.lower() == "checkerboard":
66 | engine = CheckerBoardEngine(command)
67 | engine.configure(options)
68 | else:
69 | raise ValueError(f"There is no protocol `{protocol}`.")
70 | return engine
71 |
72 | def play(self) -> List[int]:
73 | while self.round < self.games_per_pair:
74 | logger.debug(f"Playing round {self.round + 1}/{self.games_per_pair}")
75 | self.play_round()
76 | self.round += 1
77 | for player in range(self.player_count):
78 | self.scores[player] = self.results[player][0] * 2 + self.results[player][1]
79 | return self.scores
80 |
81 | def play_round(self) -> None:
82 | round_results = []
83 | for game_number, match in enumerate(self.complete_pairs[self.round]):
84 | logger.debug(f"Playing game {game_number+1}/{len(self.complete_pairs[self.round])} in {self.round+1}th round.")
85 | result1, result2 = self.play_game(self.players[match[0]], self.players[match[1]], game_number+1)
86 | round_results.append((result1, result2))
87 | # Player 1
88 | self.results[match[0]][0] += result1[0]
89 | self.results[match[0]][1] += result1[1]
90 | self.results[match[0]][2] += result1[2]
91 |
92 | # Player 2
93 | self.results[match[1]][0] += result2[0]
94 | self.results[match[1]][1] += result2[1]
95 | self.results[match[1]][2] += result2[2]
96 | self.latest_round_results = round_results.copy()
97 | logger.debug(f"Round results until now: {self.latest_round_results}")
98 | self.complete_results.append(round_results)
99 | self.latest_complete_results = self.complete_results.copy()
100 | logger.debug(f"Complete results until now: {self.latest_complete_results}")
101 |
102 | def play_game(self, player_1_info: Tuple[Union[str, List[str]], str, Dict[str, Any]],
103 | player_2_info: Tuple[Union[str, List[str]], str, Dict[str, Any]], game_number: int
104 | ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int]]:
105 | logger.debug(f"Player 1: '{player_1_info[0]}', Player 2: '{player_2_info[0]}'")
106 | board = Board(self.variant, self.starting_fen)
107 | player_1 = self.get_engine(*player_1_info)
108 | player_2 = self.get_engine(*player_2_info)
109 | if isinstance(player_1, HubEngine):
110 | player_1.init()
111 | if isinstance(player_2, HubEngine):
112 | player_2.init()
113 | player_1_limit = Limit(self.start_time, self.increment)
114 | player_2_limit = Limit(self.start_time, self.increment)
115 | max_moves = self.max_moves
116 | if max_moves == 0:
117 | max_moves = 10000
118 | while not board.is_over() and len(board.move_stack) < max_moves:
119 | logger.info(f'move: {len(board.move_stack)}')
120 | if board.turn == WHITE:
121 | start = time.perf_counter_ns()
122 | if isinstance(player_1, HubEngine):
123 | best_move = player_1.play(board, player_1_limit, False)
124 | elif isinstance(player_1, DXPEngine):
125 | best_move = player_1.play(board)
126 | else: # Checkerboard
127 | best_move = player_1.play(board, player_1_limit)
128 | end = time.perf_counter_ns()
129 | player_1_limit.time = player_1_limit.time - (end - start) / 1e9 + player_1_limit.inc
130 | else:
131 | start = time.perf_counter_ns()
132 | if isinstance(player_2, HubEngine):
133 | best_move = player_2.play(board, player_2_limit, False)
134 | elif isinstance(player_2, DXPEngine):
135 | best_move = player_2.play(board)
136 | else: # Checkerboard
137 | best_move = player_2.play(board, player_2_limit)
138 | end = time.perf_counter_ns()
139 | player_2_limit.time = player_2_limit.time - (end - start) / 1e9 + player_2_limit.inc
140 | if best_move.move:
141 | board.push(best_move.move)
142 | else:
143 | break
144 | if not isinstance(player_1, CheckerBoardEngine):
145 | player_1.quit()
146 | if not isinstance(player_2, CheckerBoardEngine):
147 | player_2.quit()
148 | player_1.kill_process()
149 | player_2.kill_process()
150 | winner = board.winner()
151 | game_ending = "1-1"
152 | if winner == WHITE:
153 | game_ending = "2-0"
154 | elif winner == BLACK:
155 | game_ending = "0-2"
156 |
157 | # Write PDN
158 | tags = self.tags.copy()
159 | tags["Result"] = game_ending
160 | tags["Round"] = f"{self.round + 1}.{game_number}"
161 | tags["White"] = str(player_1_info[0])
162 | tags["Black"] = str(player_2_info[0])
163 | tags["PlyCount"] = str(len(board.move_stack))
164 | date_tag, time_tag, utc_date_tag, utc_time_tag = self._get_date_tags()
165 | tags["Date"] = date_tag
166 | tags["Time"] = time_tag
167 | tags["UTCDate"] = utc_date_tag
168 | tags["UTCTime"] = utc_time_tag
169 | PDNWriter(self.filename, board, tags=tags, game_ending=game_ending)
170 |
171 | # Return result
172 | if winner == WHITE: # Player 1 won
173 | logger.debug("Player 1 won")
174 | return (1, 0, 0), (0, 0, 1)
175 | elif winner == BLACK: # player 2 won
176 | logger.debug("Player 2 won")
177 | return (0, 0, 1), (1, 0, 0)
178 | else: # Draw
179 | logger.debug("Game ended in a draw.")
180 | return (0, 1, 0), (0, 1, 0)
181 |
182 | def _get_date_tags(self) -> Tuple[str, str, str, str]:
183 | date = datetime.datetime.now()
184 | year = str(date.year).zfill(4)
185 | month = str(date.month).zfill(2)
186 | day = str(date.day).zfill(2)
187 | hour = str(date.hour).zfill(2)
188 | minute = str(date.minute).zfill(2)
189 | second = str(date.second).zfill(2)
190 | date_tag = f"{year}.{month}.{day}"
191 | time_tag = f"{hour}:{minute}:{second}"
192 | date = datetime.datetime.utcnow()
193 | year = str(date.year).zfill(4)
194 | month = str(date.month).zfill(2)
195 | day = str(date.day).zfill(2)
196 | hour = str(date.hour).zfill(2)
197 | minute = str(date.minute).zfill(2)
198 | second = str(date.second).zfill(2)
199 | utc_date_tag = f"{year}.{month}.{day}"
200 | utc_time_tag = f"{hour}:{minute}:{second}"
201 | return date_tag, time_tag, utc_date_tag, utc_time_tag
202 |
203 | def get_standings(self) -> List[Tuple[int, int]]:
204 | standings = list(zip(self.scores, self.int_players))
205 | standings.sort(reverse=True)
206 | return standings
207 |
208 | def print_standings(self) -> None:
209 | standings = self.get_standings()
210 | for place, engine in enumerate(standings):
211 | logger.debug(f"{place + 1}th place: {self.players[engine[1]][0]} with {engine[0]} points.")
212 | print(f"{place+1}th place: {self.players[engine[1]][0]} with {engine[0]} points.")
213 |
--------------------------------------------------------------------------------
/examples/board.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/engine_pondering.py:
--------------------------------------------------------------------------------
1 | from draughts.engine import HubEngine, Limit, PlayResult
2 | from draughts import Board
3 | import threading
4 | import time
5 |
6 | game = Board()
7 | engine = HubEngine(["scan.exe", "hub"])
8 | engine.init()
9 | limit = Limit(time=10)
10 | move = None
11 |
12 |
13 | def ponder_result(game, limit):
14 | global move
15 | move: PlayResult = engine.play(game, limit, True)
16 |
17 |
18 | thr = threading.Thread(target=ponder_result, args=(game, limit))
19 | thr.start()
20 | time.sleep(2) # Opponent thinking
21 | engine.ponderhit()
22 | thr.join()
23 |
24 | print(move.move.pdn_move)
25 |
--------------------------------------------------------------------------------
/other_licenses/ImparaAI checkers 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.
--------------------------------------------------------------------------------
/other_licenses/akalverboer DXC100_draughts_client LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Arthur Kalverboer
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/other_licenses/fishnet LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Niklas Fiekas
2 |
3 | The MIT license
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 furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | 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.
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = pydraughts
3 | version = attr: draughts.__version__
4 | author = Ioannis Pantidis
5 | description = A draughts library for Python with move generation, SVG visualizations, PDN reading and writing, engine communication and balloted openings
6 | long_description = file: README.md
7 | long_description_content_type = text/markdown
8 | keywords = checkers, draughts, game, fen, pdn
9 | url = https://github.com/AttackingOrDefending/pydraughts
10 | project_urls =
11 | Bug Tracker = https://github.com/AttackingOrDefending/pydraughts/issues
12 | classifiers =
13 | Intended Audience :: Developers
14 | Intended Audience :: Science/Research
15 | Programming Language :: Python :: 3
16 | License :: OSI Approved :: MIT License
17 | Operating System :: OS Independent
18 | Topic :: Games/Entertainment :: Board Games
19 | Topic :: Games/Entertainment :: Turn Based Strategy
20 | Topic :: Software Development :: Libraries :: Python Modules
21 | license_files =
22 | LICENSE
23 | other_licenses/*
24 |
25 | [options]
26 | include_package_data = True
27 | packages =
28 | draughts
29 | draughts.core
30 | draughts.engines
31 | draughts.engines.dxp_communication
32 | draughts.engines.checkerboard_extra
33 | draughts.ballot_files
34 | python_requires = >=3.8
35 | install_requires =
36 | msl-loadlib==0.10.0
37 |
--------------------------------------------------------------------------------
/test_pydraughts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AttackingOrDefending/pydraughts/3277325ccc17cca75e46da454dc7fb8c0c32150b/test_pydraughts/__init__.py
--------------------------------------------------------------------------------
/test_pydraughts/conftest.py:
--------------------------------------------------------------------------------
1 | import shutil
2 | import os
3 |
4 |
5 | def pytest_sessionfinish(session, exitstatus):
6 | if os.path.exists('TEMP'):
7 | shutil.rmtree('TEMP')
8 |
--------------------------------------------------------------------------------
/test_pydraughts/test_ambiguous_moves.py:
--------------------------------------------------------------------------------
1 | from draughts import Board, Move
2 |
3 |
4 | def test_ambiguous_move():
5 | game = Board(fen='W:WK47:B14,19,29,31,42')
6 | big_move = [[47, 33], [33, 24], [24, 13], [13, 36]]
7 | move_move = Move(game, big_move)
8 | assert move_move.pdn_move == "47x38x24x13x36" # Not "47x33x24x13x36"
9 |
10 | board = Board("russian", fen="W:WKd2:Bf6,c5,e5,e3:H0:F1")
11 | legal_moves = list(map(lambda m: m.pdn_move, board.legal_moves()))
12 | assert legal_moves == ['6x15x22x13', '6x15x22x9', '6x20x27x13', '6x20x27x9']
13 |
14 | board = Board("russian", fen="W:We3,g3,h4,a5,b2,h2,a3,Kf8:Bc5,e5,g5,g7,h8")
15 | legal_moves = list(map(lambda m: m.pdn_move, board.legal_moves()))
16 | assert legal_moves == ['31x13', '31x24x15x22x13', '16x21']
17 |
18 | board = Board("russian", fen="W:WKd2:Bc5,e5,c3,e3:H0:F1")
19 | legal_moves = list(map(lambda m: m.pdn_move, board.legal_moves()))
20 | assert legal_moves == ['6x13x22x15x6', '6x2', '6x15x22x13x6', '6x3']
21 |
22 | board = Board(fen="W:WK38:B18,19,32,33:H0:F1")
23 | legal_moves = list(map(lambda m: m.pdn_move, board.legal_moves()))
24 | assert legal_moves == ['38x27x13x24x38', '38x42', '38x47', '38x24x13x27x38', '38x43', '38x49']
25 |
--------------------------------------------------------------------------------
/test_pydraughts/test_ballots.py:
--------------------------------------------------------------------------------
1 | from draughts.ballots import Ballots
2 | from draughts import Board, Move
3 |
4 |
5 | def test_ballots():
6 | ballots = Ballots('english', moves=2, include_lost_games=True)
7 | ballots_returned = set()
8 | for _ in range(60):
9 | ballots_returned.add(ballots.get_ballot())
10 | assert len(ballots_returned) == 49
11 |
12 | ballots = Ballots('english', moves=3)
13 | ballots = Ballots('english', moves=4)
14 | ballots = Ballots('english', moves=5)
15 | ballots = Ballots('english', eleven_pieces=True)
16 | ballots = Ballots('italian', eleven_pieces=True)
17 | ballots = Ballots('russian')
18 | ballots = Ballots('brazilian')
19 | ballots = Ballots('russian', basic_positions=True)
20 |
21 | ballots = Ballots('english', moves=2, include_lost_games=True)
22 | for key, fen in ballots.positions.items():
23 | moves = key.split()[1:]
24 | board1 = Board('english', fen)
25 | board2 = Board('english')
26 | for move in moves:
27 | board2.push(Move(board2, pdn_move=move))
28 | assert board1._game.get_fen() == board2._game.get_fen()
29 |
--------------------------------------------------------------------------------
/test_pydraughts/test_board.py:
--------------------------------------------------------------------------------
1 | from draughts import Board
2 | from draughts.core.board import Board as InternalBoard
3 |
4 |
5 | def test_board():
6 | game = Board()
7 | game._game.board = InternalBoard()
8 | assert game.fen == 'W:W31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,10,11,12,13,14,15,16,17,18,19,2,20,3,4,5,6,7,8,9'
9 | assert game._game.board.count_movable_player_pieces() == 5
10 | game._game.board, _ = game._game.board.create_new_board_from_move([35, 30], 1, [])
11 | assert game._game.get_li_fen() == 'B:W30,31,32,33,34,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20'
12 |
13 | game = Board(fen='W:W29,31,32,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,20,23')
14 | game._game.get_possible_moves()
15 | game._game.board, _ = game._game.board.create_new_board_from_move([29, 18], 1, [])
16 | assert game._game.get_li_fen() == 'B:W18,31,32,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,19,20'
17 |
--------------------------------------------------------------------------------
/test_pydraughts/test_convert.py:
--------------------------------------------------------------------------------
1 | from draughts.convert import fen_from_variant, fen_to_variant, move_from_variant, move_to_variant, _rotate_move, _algebraic_to_number, _number_to_algebraic, _numeric_to_algebraic_square
2 |
3 |
4 | def test_convert():
5 | assert fen_from_variant('B:W19,21,22,23,25,26,27,29-31,32:B1,2,3-5,6,7,9,10,11,12',
6 | variant='english') == 'W:W21,22,23,24,26,27,28,29,30,31,32:B1,10,11,12,14,2,3,4,6,7,8'
7 | assert fen_from_variant('W:Wa3,Kc3,Ke3,g3,b2,d2,f2,h2,a1,c1,e1,g1:Bb8,d8,f8,h8,a7,c7,e7,g7,b6,Kd6,Kf6,h6',
8 | variant='russian') == 'W:W21,24,25,26,27,28,29,30,31,32,K22,K23:B1,12,2,3,4,5,6,7,8,9,K10,K11'
9 | assert fen_from_variant('W:WK12-14:BK28-31', variant='standard') == 'W:WK12,K13,K14:BK28,K29,K30,K31'
10 | assert fen_to_variant('W:W21,22,23,24,26,27-29,30,31,32:B1,10,11,12,14,2-4,6,7,8',
11 | variant='english') == 'B:W19,21,22,23,25,26,27,29,30,31,32:B1,10,11,12,2,3,4,5,6,7,9'
12 | assert fen_to_variant('B:W17,22,23,24,25,26,27-29,30,31,32:B1,2,3-5,6,7,8,9,10,11,12',
13 | variant='russian') == 'B:Wa1,c3,e3,g3,b4,c1,e1,g1,b2,d2,f2,h2:Bb6,d6,f6,h6,a7,c7,e7,g7,b8,d8,f8,h8'
14 | assert fen_to_variant('W:WK12-14:BK28-31', variant='russian') == 'W:WKa5,Kc5,Kh6:BKa1,Kc1,Ke1,Kh2'
15 |
16 | assert move_from_variant('g3-h4', variant='russian') == '24-20'
17 | assert move_to_variant('24-20', variant='russian') == 'g3-h4'
18 | assert _rotate_move('50-45', 3) == '46-41'
19 | assert _algebraic_to_number('a1a2', variant='turkish') == '1-9'
20 | assert _number_to_algebraic('1-9', variant='turkish') == 'a1-a2'
21 | assert _number_to_algebraic('a1-a2', variant='turkish') == 'a1-a2'
22 | assert _numeric_to_algebraic_square('32', width=4) == 'h8'
23 | assert _numeric_to_algebraic_square('h8', width=4) == 'h8'
24 |
--------------------------------------------------------------------------------
/test_pydraughts/test_dxp_communication.py:
--------------------------------------------------------------------------------
1 | from draughts.engines.dxp_communication.dxp_classes import DamExchange
2 | from draughts import Board
3 | import logging
4 |
5 | logging.basicConfig()
6 | logger = logging.getLogger("pydraughts")
7 | logger.setLevel(logging.DEBUG)
8 |
9 |
10 | def test_dam_exchange():
11 | dam_exchange = DamExchange()
12 |
13 | assert dam_exchange.parse('CMESSAGE') == {'type': 'C', 'text': 'MESSAGE'} # CHAT
14 | assert dam_exchange.parse('B001W') == {'type': 'B', 'moveId': '001', 'mColor': 'W'} # BACKREQ
15 | assert dam_exchange.parse('K1') == {'type': 'K', 'accCode': '1'} # BACKACC
16 | assert dam_exchange.parse('P0') == {'type': '?'} # Unknown
17 | assert dam_exchange.parse('E11') == {'type': 'E', 'reason': '1', 'stop': '1'} # GAMEEND
18 | assert (dam_exchange.parse('ANAME 1') ==
19 | {'type': 'A', 'engineName': 'NAME', 'accCode': '1'}) # GAMEACC
20 | assert (dam_exchange.parse('M000135240130') ==
21 | {'type': 'M', 'time': '0001', 'from': '35', 'to': '24', 'nCaptured': '01', 'captures': ['30']}) # MOVE
22 | assert (dam_exchange.parse(f'R01NAME Z010100BW{Board()._game.get_dxp_fen()}') ==
23 | {'type': 'R', 'name': 'NAME', 'fColor': 'Z', 'gameTime': '010', 'numMoves': '100', 'posInd': 'B',
24 | 'mColor': 'W', 'pos': 'zzzzzzzzzzzzzzzzzzzzeeeeeeeeeewwwwwwwwwwwwwwwwwwww'}) # GAMEREQ
25 |
26 | assert (dam_exchange.parse(dam_exchange.msg_gamereq(0, 100, 100, Board(), 0)) ==
27 | {'type': 'R', 'name': 'DXP Client', 'fColor': 'Z', 'gameTime': '100', 'numMoves': '100', 'posInd': 'B',
28 | 'mColor': 'W', 'pos': 'zzzzzzzzzzzzzzzzzzzzeeeeeeeeeewwwwwwwwwwwwwwwwwwww'}) # GAMEREQ
29 | assert dam_exchange.msg_gamereq(0, 100, 100) == 'R01DXP Client Z100100A' # GAMEREQ
30 | assert dam_exchange.parse(dam_exchange.msg_backreq(1, 1)) == {'type': 'B', 'moveId': '001', 'mColor': 'Z'} # BACKREQ
31 | assert dam_exchange.msg_backreq(1, 0) == 'B001W' # BACKREQ
32 | assert dam_exchange.parse(dam_exchange.msg_backacc('1')) == {'type': 'K', 'accCode': '1'} # BACKACC
33 | assert dam_exchange.msg_backacc('0') == 'K0' # BACKACC
34 | assert dam_exchange.msg_chat("chat") == "Cchat" # CHAT
35 |
--------------------------------------------------------------------------------
/test_pydraughts/test_game.py:
--------------------------------------------------------------------------------
1 | from draughts import Board, Move, WHITE
2 |
3 |
4 | def test_game():
5 | assert Board('breakthrough', 'B:WK4,31,35,36,38,40,43,44,45,46,47,48,49,50:B1,2,3,6,7,8,9,11,13,16').is_over()
6 | assert Board('breakthrough', 'B:WK4,31,35,36,38,40,43,44,45,46,47,48,49,50:B1,2,3,6,7,8,9,11,13,16').winner() == WHITE
7 |
8 | game = Board(variant='from position', fen='B:W27,28,32,34,35,37,39,40,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,12,13,15,16,19,23,24')
9 | moves = game.legal_moves()
10 | captures = list(map(lambda move: move.captures, moves))
11 | moves = list(map(lambda move: move.board_move, moves))
12 | correct_moves = [[[15, 20]], [[10, 14]], [[9, 14]], [[24, 30]], [[24, 29]], [[23, 29]], [[13, 18]], [[12, 18]], [[12, 17]], [[7, 11]], [[6, 11]], [[16, 21]]]
13 | correct_moves.sort()
14 | correct_captures = [[]] * 12
15 | assert moves == correct_moves and captures == correct_captures
16 |
17 | assert not game._game.board.is_valid_row_and_column(11, 0)
18 | assert not game._game.board.is_valid_row_and_column(5, 8)
19 | assert game._game.board.is_valid_row_and_column(5, 4)
20 | try:
21 | game._game.push([1, 46])
22 | assert False
23 | except ValueError:
24 | assert True
25 |
26 | game = Board(fen='W')
27 | assert game.legal_moves() == []
28 | game = Board()
29 | game._game.push_str_move('3530')
30 | game = Board(fen='W:W1-40:B41-50')
31 | game._game = game._game.copy()
32 | assert game.fen == 'W:W1,10,11,12,13,14,15,16,17,18,19,2,20,21,22,23,24,25,26,27,28,29,3,30,31,32,33,34,35,36,37,38,39,4,40,5,6,7,8,9:B41,42,43,44,45,46,47,48,49,50'
33 | assert game._game.board.pieces[0].get_diagonal_one_square_behind_enemy(game._game.board.pieces[10]) == []
34 |
35 | game = Board(fen='W:WK12-14:BK28-31')
36 | assert game.fen == 'W:WK12,K13,K14:BK28,K29,K30,K31'
37 |
38 | # Test all legal move conditions for italian
39 |
40 | # All rules except for 2 are tested by both fens, so the specific rule each fen tests is mentioned in a comment.
41 | # This fen tests the rule "The capture sequence that captures the most kings has to be played."
42 | game = Board('italian', 'W:W31,32,K25:B12,20,21,28,5,K13,K14,K7')
43 | assert list(map(lambda move: move.board_move, game.legal_moves())) == [[[25, 18], [18, 11], [11, 4]]]
44 | # This fen tests the rule "The capture sequence where the king occurs first has to be played."
45 | game = Board('italian', 'W:W31,32,K25:B12,13,20,21,28,7,K14,K5')
46 | assert list(map(lambda move: move.board_move, game.legal_moves())) == [[[25, 18], [18, 11], [11, 4]]]
47 |
48 | game = Board(fen='W:W6:B1')
49 | assert game._game.legal_moves() == ([], [])
50 |
51 | # Test pop()
52 | game = Board()
53 | game.push(Move(steps_move=[35, 30]))
54 | game.pop()
55 | game.push(Move(steps_move=[35, 30]))
56 | game.push(Move(steps_move=[19, 24]))
57 | game.pop()
58 | game.push(Move(steps_move=[19, 24]))
59 | game.push(Move(steps_move=[30, 19]))
60 | game.pop()
61 | game.pop()
62 | game.pop()
63 | game.push(Move(steps_move=[35, 30]))
64 | game.push(Move(steps_move=[19, 24]))
65 | game.push(Move(steps_move=[30, 19]))
66 |
67 | game = Board(fen='W:WK44:B9,18,33')
68 | game.push(Move(steps_move=[44, 22]))
69 | game.push(Move(steps_move=[22, 13]))
70 | game.pop()
71 | assert game._game._not_added_move == []
72 | assert game._game.get_fen() == 'WeeeeeeeebeeeeeeeebeeeeeeeeeeeeeebeeeeeeeeeeWeeeeee'
73 |
74 | game = Board(fen='W:WK43:BK9')
75 | game.push(Move(steps_move=[43, 49]))
76 | game.pop()
77 | assert game._game.reversible_moves == []
78 |
79 | game = Board(fen='W:WK39:B23,33')
80 | _, captures = game._game.push([[39, 28], [28, 19]])
81 | assert captures == [33, 23]
82 |
83 | game = Board()
84 | game.null()
85 | assert game._game.get_fen() == 'Bbbbbbbbbbbbbbbbbbbbbeeeeeeeeeewwwwwwwwwwwwwwwwwwww'
86 | assert game.move_stack[0].pdn_move == '0-0'
87 |
88 | game = Board('frisian', 'W:WK4,36,41,42,43,44,46,47,48,49,50:B1,2,6,12,14,17,18,23')
89 | assert (list(map(lambda move: move.board_move, game.legal_moves())) ==
90 | [[[4, 24], [24, 22], [22, 11], [11, 13], [13, 22]], [[4, 24], [24, 22], [22, 11], [11, 13], [13, 27]],
91 | [[4, 24], [24, 22], [22, 11], [11, 13], [13, 31]], [[4, 24], [24, 22], [22, 13], [13, 11], [11, 22]],
92 | [[4, 24], [24, 22], [22, 13], [13, 11], [11, 28]], [[4, 24], [24, 22], [22, 13], [13, 11], [11, 33]],
93 | [[4, 24], [24, 22], [22, 13], [13, 11], [11, 39]]])
94 |
95 | assert Board(fen='W:WKa1,K8,9:BK7,Kb2,23').fen == 'W:W9,K1,K8:B23,K6,K7'
96 |
97 | game = Board()
98 | game._game.move([31, 27], include_pdn=True)
99 | game._game.null()
100 | assert game._game.is_over() is False
101 | assert game._game.li_fen_to_hub_fen('W:WK1-3:BK4-6') == 'WWWWBBBeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
102 |
103 | # From https://github.com/AttackingOrDefending/pydraughts/issues/28
104 | game = Board('russian', 'W:Wh6:Bg7,c5,d2')
105 | assert len(game.legal_moves()) == 1
106 | assert game.legal_moves()[0].board_move == [[24, 31], [31, 13], [13, 3]]
107 |
108 |
109 | def fifty_square_draw_board(game, repeat_time=12, half_time=True):
110 | for _ in range(repeat_time):
111 | game.push(Move(steps_move=[28, 33]))
112 | game.push(Move(steps_move=[1, 7]))
113 | game.push(Move(steps_move=[33, 28]))
114 | game.push(Move(steps_move=[7, 1]))
115 | if half_time:
116 | game.push(Move(steps_move=[28, 33]))
117 | game.push(Move(steps_move=[1, 7]))
118 | return game
119 |
120 |
121 | def thirtytwo_square_draw_board(game, repeat_time=7, half_time=True):
122 | for _ in range(repeat_time):
123 | game._game.push([[32, 27]])
124 | game._game.push([[1, 6]])
125 | game._game.push([[27, 32]])
126 | game._game.push([[6, 1]])
127 | if half_time:
128 | game._game.push([[32, 27]])
129 | game._game.push([[1, 6]])
130 | return game
131 |
132 |
133 | def test_drawing_conditions():
134 | # 25 consecutive non-capture king moves.
135 | game = Board(fen='W:WK28:BK1')
136 | game = fifty_square_draw_board(game)
137 | assert game.winner() == 0
138 | game = Board(fen='B:WK1:BK28')
139 | game = fifty_square_draw_board(game)
140 | assert game.winner() == 0
141 |
142 | # 1 king vs 3 pieces (with at least 1 king) and 16 moves made.
143 | game = Board(fen='W:WK28:BK1,2,3')
144 | game = fifty_square_draw_board(game, 8, False)
145 | assert game.winner() == 0
146 | game = Board(fen='B:WK1,K2,K3:BK28')
147 | game = fifty_square_draw_board(game, 8, False)
148 | assert game.winner() == 0
149 |
150 | # 1 king vs 2 or fewer pieces (with at least 1 king) and 5 moves made.
151 | game = Board(fen='W:WK28:BK1,2')
152 | game = fifty_square_draw_board(game, 2)
153 | assert game.winner() == 0
154 | game = Board(fen='B:WK1,K2:BK28')
155 | game = fifty_square_draw_board(game, 2)
156 | assert game.winner() == 0
157 |
158 | # 2 kings vs 1 king and 7 moves made.
159 | game = Board('frisian', 'W:WK28:BK1,K2')
160 | game = fifty_square_draw_board(game, 3)
161 | assert game.winner() == 0
162 | game = Board('frisian', 'B:WK1,K2:BK28')
163 | game = fifty_square_draw_board(game, 3)
164 | assert game.winner() == 0
165 |
166 | # 3 or more kings vs 1 king and 15 moves made.
167 | game = Board('russian', 'W:WKg1:BKb8,Kd8,Kf8')
168 | game = thirtytwo_square_draw_board(game)
169 | assert game.winner() == 0
170 | game = Board('russian', 'B:WKb8,Kd8,Kf8:BKg1')
171 | game = thirtytwo_square_draw_board(game)
172 | assert game.winner() == 0
173 |
174 | # 15 consecutive non-capture king moves.
175 | game = Board('russian', 'W:WKg1:BKb8')
176 | game = thirtytwo_square_draw_board(game)
177 | assert game.winner() == 0
178 |
179 | # Same number of kings, same number of pieces, 4 or 5 pieces per side and 30 moves made.
180 | game = Board('russian', 'W:Wa1,c1,e1,Kg1:BKb8,d8,f8,h8')
181 | game._game.moves_since_last_capture = 60
182 | assert game.winner() == 0
183 |
184 | # Same number of kings, same number of pieces, 6 or 7 pieces per side and 60 moves made.
185 | game = Board('russian', 'W:Wb2,d2,a1,c1,e1,Kg1:BKb8,d8,f8,h8,e7,g7')
186 | game._game.moves_since_last_capture = 120
187 | assert game.winner() == 0
188 |
189 | # 3 pieces (with at least 1 king) vs 1 king on the long diagonal.
190 | game = Board('russian', 'W:WKa7,b4,f4:BKa1')
191 | for _ in range(2):
192 | game._game.push([[5, 9]])
193 | game._game.push([[29, 4]])
194 | game._game.push([[9, 5]])
195 | game._game.push([[4, 29]])
196 | game._game.push([[5, 9]])
197 | game._game.push([[29, 4]])
198 | assert game.winner() == 0
199 | game = Board('russian', 'B:WKa1:BKa7,b4,f4')
200 | for _ in range(2):
201 | game._game.push([[5, 9]])
202 | game._game.push([[29, 4]])
203 | game._game.push([[9, 5]])
204 | game._game.push([[4, 29]])
205 | game._game.push([[5, 9]])
206 | game._game.push([[29, 4]])
207 | assert game.winner() == 0
208 |
209 | # 2 pieces (with at least 1 king) vs 1 king and 5 moves made.
210 | game = Board('russian', 'W:WKg1:BKb8,h8')
211 | game = thirtytwo_square_draw_board(game, 2)
212 | assert game.winner() == 0
213 | game = Board('russian', 'B:WKb8,a1:BKg1')
214 | game = thirtytwo_square_draw_board(game, 2)
215 | assert game.winner() == 0
216 |
217 | game = Board('english', 'B:WK32:BK1')
218 | game = thirtytwo_square_draw_board(game, 20, False)
219 | assert game.winner() == 0
220 |
221 | game = Board('turkish', 'W:WKh5:Bb7')
222 | game._game.push([[32, 31]])
223 | game._game.push([[10, 9]])
224 | game._game.push([[31, 32]])
225 | game._game.push([[9, 10]])
226 | assert game.winner() == 0
227 |
--------------------------------------------------------------------------------
/test_pydraughts/test_move.py:
--------------------------------------------------------------------------------
1 | from draughts import Board, Move
2 | from draughts.core.game import Game
3 | from draughts.core.move import StandardMove
4 |
5 |
6 | def test_move():
7 | game = Board(fen='W:W42:B28,38')
8 | assert Move(game, steps_move=[42, 33, 22]).board_move == [[42, 33], [33, 22]]
9 | assert Move(game, li_api_move=['4233', '3322']).board_move == [[42, 33], [33, 22]]
10 | assert Move(game, li_one_move='423322').board_move == [[42, 33], [33, 22]]
11 |
12 | assert Move(steps_move=[42, 33, 22]).board_move == [[42, 33], [33, 22]]
13 | assert Move(li_api_move=['4233', '3322']).board_move == [[42, 33], [33, 22]]
14 | assert Move(li_one_move='423322').board_move == [[42, 33], [33, 22]]
15 |
16 | assert Move(hub_position_move='3530').hub_move == '35-30'
17 | assert Move(hub_move='42x33x22').hub_position_move == '423322'
18 | assert Move(hub_move='35-30').hub_position_move == '3530'
19 |
20 | assert Move(pdn_position_move='3530').pdn_move == '35-30'
21 |
22 | # From https://github.com/AttackingOrDefending/pydraughts/issues/23 Converted to board
23 | game = Board(variant='brazilian', fen='B:Wf6,b4,d4,a3,b2,d2,h2,a1,c1,e1,g1:Bb8,d8,f8,h8,c7,e7,g7,b6,d6,h6')
24 | move_ordered = Move(board=game, hub_move='28x17x13x14x23')
25 | move_unordered = Move(board=game, hub_move='28x17x14x13x23')
26 | assert move_ordered.board_move == move_unordered.board_move
27 |
28 |
29 | def test_standard_move():
30 | game = Game(fen='W:W42:B28,38')
31 | assert StandardMove(game, steps_move=[42, 33, 22]).board_move == [[42, 33], [33, 22]]
32 | assert StandardMove(game, li_api_move=['4233', '3322']).board_move == [[42, 33], [33, 22]]
33 | assert StandardMove(game, li_one_move='423322').board_move == [[42, 33], [33, 22]]
34 |
35 | assert StandardMove(steps_move=[42, 33, 22]).board_move == [[42, 33], [33, 22]]
36 | assert StandardMove(li_api_move=['4233', '3322']).board_move == [[42, 33], [33, 22]]
37 | assert StandardMove(li_one_move='423322').board_move == [[42, 33], [33, 22]]
38 |
39 | assert StandardMove(hub_position_move='3530').hub_move == '35-30'
40 | assert StandardMove(hub_move='42x33x22').hub_position_move == '423322'
41 | assert StandardMove(hub_move='35-30').hub_position_move == '3530'
42 |
43 | assert StandardMove(pdn_position_move='3530').pdn_move == '35-30'
44 |
45 | # From https://github.com/AttackingOrDefending/pydraughts/issues/23
46 | game = Game(variant='brazilian', fen='Bbbbbebbbbbwbeeeewweeweeewwewwwww')
47 | move_ordered = StandardMove(board=game, hub_move='8x13x11x17x18')
48 | move_unordered = StandardMove(board=game, hub_move='8x13x11x18x17')
49 | assert move_ordered.board_move == move_unordered.board_move
50 | assert StandardMove(Game(), hub_move='31-27').hub_position_move == '3127'
51 | assert StandardMove(Game(), pdn_move='31-27').pdn_position_move == '3127'
52 | assert StandardMove(Game(fen='W:WK47:B14,19,29,31,42'), pdn_move='47x38x24x13x36').pdn_position_move == '4738241336'
53 | assert StandardMove(Game(fen='W:WK47:B14,19,29,31,42'), pdn_position_move='4738200936').pdn_move == '47x38x20x9x36'
54 |
--------------------------------------------------------------------------------
/test_pydraughts/test_pdn.py:
--------------------------------------------------------------------------------
1 | from draughts import Move, Board
2 | from draughts.PDN import PDNReader, PDNWriter
3 |
4 | import requests
5 | import zipfile
6 | import os
7 | import shutil
8 | import logging
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | def download_games():
14 | headers = {'User-Agent': 'User Agent', 'From': 'mail@mail.com'}
15 | response = requests.get('https://github.com/wiegerw/pdn/raw/refs/heads/master/games/games.zip', headers=headers, allow_redirects=True)
16 | with open('./TEMP/games.zip', 'wb') as file:
17 | file.write(response.content)
18 | with zipfile.ZipFile('./TEMP/games.zip', 'r') as zip_ref:
19 | zip_ref.extractall('./games/')
20 |
21 |
22 | if os.path.exists('TEMP'):
23 | shutil.rmtree('TEMP')
24 | os.mkdir('TEMP')
25 | try:
26 | download_games()
27 | except Exception:
28 | download_games() # Attempt to download twice if there are problems.
29 |
30 |
31 | def test_pdn_reading():
32 | files = os.listdir('./games/succeed')
33 | for file in files:
34 | filepath = os.path.realpath(f'./games/succeed/{file}')
35 | PDNReader(filename=filepath)
36 |
37 |
38 | def test_pdn_writing():
39 | filepath = os.path.realpath('./games/succeed/mrcd2000kval.pdn')
40 | games = PDNReader(filename=filepath, encodings='utf8')
41 | one_game = games.games[1]
42 | assert one_game.get_titles() == ["", ""]
43 | assert one_game.get_ratings() == ["", ""]
44 | assert one_game.get_na() == ["", ""]
45 | assert one_game.get_types() == ["", ""]
46 | moves = one_game.moves
47 | original_moves = moves
48 | game = Board(variant='russian')
49 | for move in moves:
50 | game.push(Move(pdn_move=move, board=game, variant=game.variant))
51 |
52 | PDNWriter('pdn_writer.pdn', game, game_ending=one_game.game_ending)
53 |
54 | with open('pdn_writer.pdn') as file:
55 | data = file.read().split('\n')
56 | data = list(filter(lambda line: bool(line) and not line.startswith('['), data))
57 | moves = data[0]
58 |
59 | correct_moves = '1. c3-d4 f6-e5 2. d4xf6 g7xe5 3. b2-c3 h8-g7 4. g3-f4 e5xg3 5. h2xf4 g7-f6 6. a1-b2 b6-a5 7. e3-d4 d6-e5 8. f4xd6 c7xe5 9. d4-c5 b8-c7 10. d2-e3 e5-f4 11. e3xg5 h6xf4 12. c3-d4 c7-b6 13. c1-d2 f6-e5 14. d4xf6 e7xg5 15. f2-e3 b6xf2 16. e1xe5 a7-b6 17. e5-d6 f8-e7 18. d6xf8 b6-c5 19. f8xb4 a5xe1 20. g1-f2 e1xg3 0-1'
60 | assert moves == correct_moves
61 |
62 | PDNWriter('pdn_writer_moves.pdn', moves=original_moves, game_ending=one_game.game_ending, starting_fen="")
63 |
64 | with open('pdn_writer_moves.pdn') as file:
65 | data = file.read().split('\n')
66 | data = list(filter(lambda line: bool(line) and not line.startswith('['), data))
67 | moves = data[0]
68 |
69 | correct_moves = '1. c3-d4 f6-e5 2. d4xf6 g7xe5 3. b2-c3 h8-g7 4. g3-f4 e5xg3 5. h2xf4 g7-f6 6. a1-b2 b6-a5 7. e3-d4 d6-e5 8. f4xd6 c7xe5 9. d4-c5 b8-c7 10. d2-e3 e5-f4 11. e3xg5 h6xf4 12. c3-d4 c7-b6 13. c1-d2 f6-e5 14. d4xf6 e7xg5 15. f2-e3 b6xd4xf2 16. e1xg3xe5 a7-b6 17. e5-d6 f8-e7 18. d6xf8 b6-c5 19. f8xb4 a5xc3xe1 20. g1-f2 e1xg3 0-1'
70 | assert moves == correct_moves
71 |
72 | for variant in ["frysk!", "turkish", "brazilian", "english", "standard"]:
73 | pdn_writer = PDNWriter('pdn_writer_minor_test.pdn', variant=variant, moves=[])
74 | # We use [1:] in the fen, so we exclude the starting color, because internally white always starts, so Game will
75 | # always return a fen with white starting, even in english.
76 | assert pdn_writer._startpos_to_fen() == Board(variant).fen
77 |
78 | PDNWriter('pdn_writer_minor_test_2.pdn', moves=["35-30"])
79 | with open('pdn_writer_minor_test_2.pdn') as file:
80 | data = file.read().split('\n')
81 | data = list(filter(lambda line: bool(line) and not line.startswith('['), data))
82 | moves = data[0]
83 |
84 | correct_moves = '1. 35-30 *'
85 | assert moves == correct_moves
86 |
87 | PDNWriter('pdn_writer_minor_test_3.pdn', moves=["20-25"], starting_fen='B:W31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20:H0:F1')
88 | with open('pdn_writer_minor_test_3.pdn') as file:
89 | data = file.read().split('\n')
90 | data = list(filter(lambda line: bool(line) and not line.startswith('['), data))
91 | moves = data[0]
92 |
93 | correct_moves = '1... 20-25 *'
94 | assert moves == correct_moves
95 |
--------------------------------------------------------------------------------
/test_pydraughts/test_piece.py:
--------------------------------------------------------------------------------
1 | from draughts import Board
2 |
3 |
4 | def test_piece():
5 | game = Board(fen='W:WK28:B19,37')
6 | assert (list(map(lambda move: move.board_move, game.legal_moves())) ==
7 | [[[28, 14]], [[28, 10]], [[28, 5]], [[28, 41]], [[28, 46]]])
8 |
--------------------------------------------------------------------------------
/test_pydraughts/test_repr.py:
--------------------------------------------------------------------------------
1 | from draughts import Board
2 |
3 |
4 | def test_repr():
5 | game = Board(fen="B:W27,34,35,36,37,39,40,41,42,43,44,45,46,47,48,49,50:B1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,18,19,20")
6 | assert str(game) == """ | b | | b | | b | | b | | b
7 | ---------------------------------------
8 | b | | b | | b | | b | | b |
9 | ---------------------------------------
10 | | b | | b | | b | | b | | b
11 | ---------------------------------------
12 | b | | | | b | | b | | b |
13 | ---------------------------------------
14 | | | | | | | | | |
15 | ---------------------------------------
16 | | | w | | | | | | |
17 | ---------------------------------------
18 | | | | | | | | w | | w
19 | ---------------------------------------
20 | w | | w | | | | w | | w |
21 | ---------------------------------------
22 | | w | | w | | w | | w | | w
23 | ---------------------------------------
24 | w | | w | | w | | w | | w | """
25 | game = Board(variant="italian", fen="W:W29,32:BK15")
26 | assert str(game) == """ | | | | | | |
27 | -------------------------------
28 | | | | | | | |
29 | -------------------------------
30 | | | | | | | |
31 | -------------------------------
32 | | | | | | B | |
33 | -------------------------------
34 | | | | | | | |
35 | -------------------------------
36 | | | | | | | |
37 | -------------------------------
38 | | | | | | | |
39 | -------------------------------
40 | | w | | | | | | w """
41 | game = Board(variant="english", fen="W:W29,32:BK14")
42 | assert str(game) == """ | w | | | | | | w
43 | -------------------------------
44 | | | | | | | |
45 | -------------------------------
46 | | | | | | | |
47 | -------------------------------
48 | | | | | | | |
49 | -------------------------------
50 | | | | | | B | |
51 | -------------------------------
52 | | | | | | | |
53 | -------------------------------
54 | | | | | | | |
55 | -------------------------------
56 | | | | | | | | """
57 | game = Board(variant="russian", fen="W:Wb2,Kc3:Bd4,e5,b8,f8")
58 | assert str(game) == """ | b | | | | b | |
59 | -------------------------------
60 | | | | | | | |
61 | -------------------------------
62 | | | | | | | |
63 | -------------------------------
64 | | | | | b | | |
65 | -------------------------------
66 | | | | b | | | |
67 | -------------------------------
68 | | | W | | | | |
69 | -------------------------------
70 | | w | | | | | |
71 | -------------------------------
72 | | | | | | | | """
73 | game = Board(variant="turkish", fen="W:Wa3,Kb3:Ba7,c7")
74 | assert str(game) == """ | | | | | | |
75 | -------------------------------
76 | b | | b | | | | |
77 | -------------------------------
78 | | | | | | | |
79 | -------------------------------
80 | | | | | | | |
81 | -------------------------------
82 | | | | | | | |
83 | -------------------------------
84 | w | W | | | | | |
85 | -------------------------------
86 | | | | | | | |
87 | -------------------------------
88 | | | | | | | | """
89 |
--------------------------------------------------------------------------------
/test_pydraughts/test_svg.py:
--------------------------------------------------------------------------------
1 | from draughts import Board, Move
2 | from draughts.svg import create_svg
3 |
4 |
5 | def test_svg():
6 | board = Board(fen="W:WK4,19,27,33,34,47:B11,12,17,22,25,26:H0:F50")
7 | board.push(Move(board, pdn_move="27x16"))
8 | svg = create_svg(board)
9 | assert svg == """"""
149 |
--------------------------------------------------------------------------------
/test_pydraughts/test_tournament.py:
--------------------------------------------------------------------------------
1 | from draughts.tournament import RoundRobin
2 | import pytest
3 | import sys
4 | import logging
5 | platform = sys.platform
6 | file_extension = '.exe' if platform == 'win32' else ''
7 |
8 | logging.basicConfig()
9 | logger = logging.getLogger("pydraughts")
10 | logger.setLevel(logging.DEBUG)
11 |
12 |
13 | @pytest.mark.timeout(300, method="thread")
14 | def test_tournament():
15 | if platform != 'win32':
16 | assert True
17 | return
18 | players = [(["scan.exe", "dxp"], "dxp", {'engine-opened': False}, None), ("kr_hub.exe", "hub", {}, None)]
19 | tournament = RoundRobin("tournament.pdn", players, 20, .2)
20 | scores = tournament.play()
21 | logger.debug(f"Scores: {scores}")
22 | tournament.print_standings()
23 |
--------------------------------------------------------------------------------
/test_pydraughts/test_variants.py:
--------------------------------------------------------------------------------
1 | import draughts
2 | from draughts import Board, Move
3 |
4 |
5 | def play_game(moves, variant):
6 | game = Board(variant)
7 | for move in moves:
8 | assert game.winner() is None
9 | game.push(Move(pdn_move=move, board=game))
10 | game.winner()
11 | return game
12 |
13 |
14 | def test_variants():
15 | # Standard
16 | standard_moves = '32-28 17-21 34-29 21-26 40-34 16-21 37-32 26x37 42x31 21-26 47-42 26x37 42x31 11-17 44-40 6-11 41-37 17-21 31-26 21-27 32x21 18-23 29x18 12x41 46x37 13-18 38-32 9-13 43-38 19-23 49-43 14-19 34-29 23x34 39x30 20-25 50-44 25x34 40x29 10-14 33-28 7-12 21-16 2-7 28-23 19x28 32x23 14-20 29-24 20x29 23x34 5-10 37-32 15-20 44-39 13-19 36-31 11-17 38-33 8-13 45-40 19-24 43-38 13-19 32-28 18-23 28-22 17x28 33x22 20-25 48-42 12-18 22x13 19x8 39-33 10-14 33-29 24x33 38x18 4-9 42-38 14-19 38-33 19-24 26-21 8-13 18-12 7x18 34-29 18-22 29x20 25x14 16-11 13-18 40-34 9-13 33-29 14-19 21-16 22-27 31x22 18x27 34-30 27-32 29-24 32-37 24-20 37-41 20-15 41-46 15-10 19-24 30x8 3x12 10-4 1-6 4-13 6x17 35-30 46-41 30-25 41-47 13-27 17-21 16-11 21x32 11-6 47-15 6-1 12-17 25-20 15x24 1-40 17-22 40-35 24-29 35-13 22-27 13x31 32-38 31-27 38-42 27-32 42-48 32-28 48-34 28-19 34-25 19-28'.split()
17 | play_game(standard_moves, 'standard')
18 | # Frisian
19 | frisian_moves = '32-27 19-23 27-21 17x26 31-27 26x28 33x24 20x29 39x17 12x21 34-30 21-26 30-25 13-18 38-32 8-12 40-34 12-17 34-30 7-12 43-38 14-20 25x14 9x40 45x34 17-21 34-30 18-22 30-25 12-17 32x12 17x8 38-33 21-27 37x17 11x22 33-29 22-27 29-24 2-7 42-37 16-21 36x16 6x26 37x17 21x12 41-36 1-6 36x16 6x26 46-41 12-17 41-37 17-22 37-32 22x42 48x37 8-12 50-45 3-9 44-39 10-14 24-20 15x24 25x23 14-20 49-44 12-17 44-40 17-22 23x21 26x17 39-34 17-21 37-31 21x41 47x36 4-10 34-30 9-13 30-25 7-12 25x14 10x19 35-30 19-23 36-31 12-17 30-25 23-29 40-34 29x40 45x34 5-10 34-29 10-14 29-24 14x34 25-20 34-40 20-15 40-45 15-10 13-19 10-5 19-24 5-32 24-29 32-49 29-33 49-43 17-21 43x26 45-50 31-27 50-39 26-48 39-50 48-37 50-44 37-19 44-49'.split()
20 | game = play_game(frisian_moves, 'frisian')
21 | assert list(map(lambda move: move.board_move, game.legal_moves())) == [[[27, 21]], [[27, 22]]]
22 | # Frysk!
23 | frysk_moves = '48-43 1-7 43-39 7-11 46-41 11-17 41-37 17-21 37-31 21x41 47x36 5-10 36-31 3-8 49-44 10-14 44-40 8-12 40-35 14-20 39-33 20-24 50-45 4-9 33-28 2-8 28-22 12x32 31x33 8-12 35-30 24x35 45x25 9-14 33-28 12-17 28-23 17-21 23-18 21-27 18-12 27-31 12-7 31-37 7-1 37-42 1-18 42-47 18-4 47-36 4x34 36-41 25-20 41-10 20-15 10-19 34-12 19-28 12-34 28-19 15-10 19x5 34-18 5-14 18-22 14-10'.split()
24 | play_game(frysk_moves, 'frysk')
25 | # Antidraughts
26 | antidraughts_moves = '35-30 16-21 31-27 20-24 27x16 24x35 36-31 15-20 31-26 20-25 41-36 17-21 16x27 10-15 33-28 18-22 27x18 13x33 38x29 12-18 29-24 19x30 32-27 18-22 27x18 5-10 43-38 8-12 18-13 9x18 36-31 18-22 49-43 22-27 31x22 12-18 22x13 14-19 13x24 30x19 34-29 11-17 26-21 17x26 29-24 19x30 37-32 6-11 46-41 7-12 38-33 30-34 40x29 12-17 45-40 2-8 50-45 15-20 41-36 1-7 36-31 26x28 33x22 17x28 39-33 28x50 42-37 35x44 37-31 8-12 29-24 20x29 45-40 44x35 43-39 50x36 47-41 36x47 48-42 47x33'.split()
27 | game = play_game(antidraughts_moves, 'antidraughts')
28 | assert game.winner() == draughts.WHITE
29 | # Breakthrough
30 | breakthrough_moves = '32-28 19-24 37-32 17-21 34-29 21-26 39-34 26x37 42x31 18-22 28x17 11x22 44-39 12-18 47-42 14-19 50-44 10-14 41-37 7-12 34-30 4-10 46-41 1-7 30-25 19-23 40-34 14-19 25x14 9x20 32-28 23x32 37x17 12x21 34-30 21-26 41-37 20-25 29x20 25x34 39x30 15x24 45-40 16-21 38-32 6-11 43-38 11-16 49-43 7-12 40-34 3-9 43-39 10-14 33-29 24x33 38x29 2-7 30-24 19x30 35x24 5-10 42-38 18-22 34-30 21-27 32x21 16x27 29-23 10-15 30-25 27-32 38x18 13x22 48-42 22-28 23x32 8-13 39-33 14-19 33-28 19x30 25x34 7-11 31-27 9-14 34-29 14-20 29-23 20-25 27-22 12-17 22-18 13x33 23-19 26-31 37x26 33-39 44x33 17-22 19-13 25-30 13-9 22-27 32x21 11-17 21x12 15-20 9-4'.split()
31 | play_game(breakthrough_moves, 'breakthrough')
32 | # Russian
33 | russian_moves = 'a3-b4 b6-a5 g3-f4 d6-e5 f4xd6 e7xa3 e3-d4 c7-d6 c3-b4 a5xe5 h2-g3 f6-g5 g3-h4 g5-f4 f2-g3 f4xh2 d2-c3 a7-b6 c3-b4 a3xc5 g1-f2 h6-g5 h4xd4 c5xg1 e1-f2 g1xe3 c1-d2 e3xa3 a1-b2 a3xc1'.split()
34 | play_game(russian_moves, 'russian')
35 | # Brazilian
36 | brazilian_moves = 'g3-f4 d6-e5 f4xd6 c7xe5 a3-b4 e7-d6 b4-a5 b8-c7 c3-d4 e5xc3 b2xd4 b6-c5 d4xb6 a7xc5 h2-g3 f6-e5 g3-f4 e5xg3 f2xh4 h6-g5 h4xf6 g7xe5 a5-b6 c7xa5 c1-b2 d8-e7 b2-a3 f8-g7 a3-b4 a5xc3 d2xb4 c5xa3 g1-f2 e5-d4 e3xc5 d6xb4 f2-g3 g7-f6 e1-d2 f6-e5 a1-b2 a3xe3 g3-f4'.split()
37 | play_game(brazilian_moves, 'brazilian')
38 | # English/American
39 | english_moves = '11-15 23-18 8-11 27-23 4-8 23-19 10-14 19x10 14x23 26x19 7x14 24-20 6-10 22-17 9-13 30-26 13x22 25x9 5x14 29-25 14-18 26-23 18x27 32x23 11-15 28-24 2-6 21-17 8-11 25-22 6-9 23-18 1-5 17-13 9-14 18x9 5x14 13-9 14-18 22-17 18-23 9-6 15-18 6-2 18-22 2-6 3-8 6x15 11x18 19-15 22-26 31x22 18x25 24-19 23-27 20-16 27-31 16-11 31-27 11x4 27-23'.split()
40 | play_game(english_moves, 'american')
41 | # Italian
42 | italian_moves = '21-17 12-15 23-19 11-14 19x12 8x15 28-23 14-18 22x13 9x18 23-20 10-14 20x11 6x15 17-13 3-6 13-9 5-10 32-28 1-5 26-21 7-12 21-17 12-16 30-26 6-11 26-21 4-7 29-26 14-19 21x14 11x18 26-21 16-20 21x14 19-22 27x18 10x19 31-27 19-22 28-23 22x31 23x16 31-27 18-14 27-23 25-21 23-20 17-13 15-19 21-18 19-22 13-10 22-26 10x1 26-30 1-5 30-27 5-10 27-22 10-13 22-19 14-10 19-15 10-5 20-23 5-1 23-20 1-5 7-11 5-10 15-12 10-6 11-15 6-11'.split()
43 | play_game(italian_moves, 'italian')
44 | # Turkish
45 | turkish_moves = ['e3-e4', 'f6-f5', 'b3-b4', 'f5-g5', 'f3-f4', 'a6-a5', 'a3-a4', 'a5xa1', 'b4-a4', 'a1xa6', 'e4-e5', 'e6xg4', 'c3-b3', 'a6-a3', 'd3-d4', 'a3xe1', 'd2-d3', 'e1-f1', 'd4-e4']
46 | play_game(turkish_moves, 'turkish')
47 |
--------------------------------------------------------------------------------