SanaomerUnity commited on
Commit
dde3070
·
1 Parent(s): 428245c

Upload 19 files

Browse files
backgammon-1.0.0/.DS_Store ADDED
Binary file (6.15 kB). View file
 
backgammon-1.0.0/LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
backgammon-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: backgammon
3
+ Version: 1.0.0
4
+ Summary: Backgammon engine for the Backgammon Network.
5
+ Home-page: https://github.com/softwerks/backgammon
6
+ Author: Softwerks
7
+ Author-email: info@softwerks.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+
17
+ backgammon
18
+ ==========
19
+
20
+ Backgammon engine for the `Backgammon Network <https://www.bkgmn.net>`_.
21
+
22
+ Installation
23
+ ------------
24
+
25
+ .. code-block:: bash
26
+
27
+ $ pip install backgammon
28
+
29
+ Getting Started
30
+ ---------------
31
+
32
+ .. code-block:: pycon
33
+
34
+ >>> import backgammon
35
+ >>> b = backgammon.Backgammon("4OvgATDgc+QBUA", "cInpAAAAAAAA")
36
+ >>> print(b)
37
+ Position ID: 4OvgATDgc+QBUA
38
+ Match ID : cInpAAAAAAAA
39
+ +13-14-15-16-17-18------19-20-21-22-23-24-+
40
+ | X O O | | O X |
41
+ | X O | | O |
42
+ | X O | | O |
43
+ | X | | O |
44
+ | | | O |
45
+ v| |BAR| |
46
+ | | | X |
47
+ | O | | X |
48
+ | O X | | X |
49
+ | O X | | X O |
50
+ | O X X | X | X O |
51
+ +12-11-10--9--8--7-------6--5--4--3--2--1-+
52
+ >>> for play in b.generate_plays():
53
+ ... print(play.moves)
54
+ ... print(play.position)
55
+ ...
56
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=13, destination=11))
57
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 2, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
58
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=24, destination=21))
59
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 1, 0, 1, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
60
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=11, destination=9))
61
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 1, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
62
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=22, destination=20))
63
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 1, 0, 0, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
64
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=13, destination=10))
65
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 1, 1, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
66
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=8, destination=5))
67
+ Position(board_points=(-2, 0, 0, 0, 1, 5, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
68
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=24, destination=22))
69
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 2, 0, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
70
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=6, destination=4))
71
+ Position(board_points=(-2, 0, 0, 1, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
72
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=6, destination=3))
73
+ Position(board_points=(-2, 0, 1, 0, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
74
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=11, destination=8))
75
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
76
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=8, destination=6))
77
+ Position(board_points=(-2, 0, 0, 0, 0, 6, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
78
+
79
+
backgammon-1.0.0/README.rst ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ backgammon
2
+ ==========
3
+
4
+ Backgammon engine for the `Backgammon Network <https://www.bkgmn.net>`_.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ .. code-block:: bash
10
+
11
+ $ pip install backgammon
12
+
13
+ Getting Started
14
+ ---------------
15
+
16
+ .. code-block:: pycon
17
+
18
+ >>> import backgammon
19
+ >>> b = backgammon.Backgammon("4OvgATDgc+QBUA", "cInpAAAAAAAA")
20
+ >>> print(b)
21
+ Position ID: 4OvgATDgc+QBUA
22
+ Match ID : cInpAAAAAAAA
23
+ +13-14-15-16-17-18------19-20-21-22-23-24-+
24
+ | X O O | | O X |
25
+ | X O | | O |
26
+ | X O | | O |
27
+ | X | | O |
28
+ | | | O |
29
+ v| |BAR| |
30
+ | | | X |
31
+ | O | | X |
32
+ | O X | | X |
33
+ | O X | | X O |
34
+ | O X X | X | X O |
35
+ +12-11-10--9--8--7-------6--5--4--3--2--1-+
36
+ >>> for play in b.generate_plays():
37
+ ... print(play.moves)
38
+ ... print(play.position)
39
+ ...
40
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=13, destination=11))
41
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 2, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
42
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=24, destination=21))
43
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 1, 0, 1, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
44
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=11, destination=9))
45
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 1, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
46
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=22, destination=20))
47
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 1, 0, 0, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
48
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=13, destination=10))
49
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 1, 1, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
50
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=8, destination=5))
51
+ Position(board_points=(-2, 0, 0, 0, 1, 5, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
52
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=24, destination=22))
53
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 2, 0, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
54
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=6, destination=4))
55
+ Position(board_points=(-2, 0, 0, 1, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
56
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=6, destination=3))
57
+ Position(board_points=(-2, 0, 1, 0, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
58
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=11, destination=8))
59
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
60
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=8, destination=6))
61
+ Position(board_points=(-2, 0, 0, 0, 0, 6, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
backgammon-1.0.0/backgammon.egg-info/PKG-INFO ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.1
2
+ Name: backgammon
3
+ Version: 1.0.0
4
+ Summary: Backgammon engine for the Backgammon Network.
5
+ Home-page: https://github.com/softwerks/backgammon
6
+ Author: Softwerks
7
+ Author-email: info@softwerks.com
8
+ License: UNKNOWN
9
+ Platform: UNKNOWN
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.7
14
+ Description-Content-Type: text/x-rst
15
+ License-File: LICENSE
16
+
17
+ backgammon
18
+ ==========
19
+
20
+ Backgammon engine for the `Backgammon Network <https://www.bkgmn.net>`_.
21
+
22
+ Installation
23
+ ------------
24
+
25
+ .. code-block:: bash
26
+
27
+ $ pip install backgammon
28
+
29
+ Getting Started
30
+ ---------------
31
+
32
+ .. code-block:: pycon
33
+
34
+ >>> import backgammon
35
+ >>> b = backgammon.Backgammon("4OvgATDgc+QBUA", "cInpAAAAAAAA")
36
+ >>> print(b)
37
+ Position ID: 4OvgATDgc+QBUA
38
+ Match ID : cInpAAAAAAAA
39
+ +13-14-15-16-17-18------19-20-21-22-23-24-+
40
+ | X O O | | O X |
41
+ | X O | | O |
42
+ | X O | | O |
43
+ | X | | O |
44
+ | | | O |
45
+ v| |BAR| |
46
+ | | | X |
47
+ | O | | X |
48
+ | O X | | X |
49
+ | O X | | X O |
50
+ | O X X | X | X O |
51
+ +12-11-10--9--8--7-------6--5--4--3--2--1-+
52
+ >>> for play in b.generate_plays():
53
+ ... print(play.moves)
54
+ ... print(play.position)
55
+ ...
56
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=13, destination=11))
57
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 2, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
58
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=24, destination=21))
59
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 1, 0, 1, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
60
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=11, destination=9))
61
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 1, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
62
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=22, destination=20))
63
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 1, 0, 0, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
64
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=13, destination=10))
65
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 1, 1, -4, 3, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
66
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=8, destination=5))
67
+ Position(board_points=(-2, 0, 0, 0, 1, 5, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
68
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=24, destination=22))
69
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 2, 0, 0), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
70
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=6, destination=4))
71
+ Position(board_points=(-2, 0, 0, 1, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
72
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=6, destination=3))
73
+ Position(board_points=(-2, 0, 1, 0, 0, 4, 0, 3, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
74
+ (Move(pips=2, source=None, destination=23), Move(pips=3, source=11, destination=8))
75
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 4, 0, 0, 0, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 0, 1, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
76
+ (Move(pips=3, source=None, destination=22), Move(pips=2, source=8, destination=6))
77
+ Position(board_points=(-2, 0, 0, 0, 0, 6, 0, 2, 0, 0, 1, -4, 4, 0, 0, 0, -3, -1, -5, 0, 0, 1, 0, 1), player_bar=0, player_off=1, opponent_bar=0, opponent_off=0)
78
+
79
+
backgammon-1.0.0/backgammon.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ LICENSE
2
+ README.rst
3
+ setup.py
4
+ backgammon/__init__.py
5
+ backgammon/__main__.py
6
+ backgammon/backgammon.py
7
+ backgammon/match.py
8
+ backgammon/position.py
9
+ backgammon.egg-info/PKG-INFO
10
+ backgammon.egg-info/SOURCES.txt
11
+ backgammon.egg-info/dependency_links.txt
12
+ backgammon.egg-info/top_level.txt
13
+ tests/__init__.py
14
+ tests/test_backgammon.py
15
+ tests/test_match.py
16
+ tests/test_position.py
backgammon-1.0.0/backgammon.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
backgammon-1.0.0/backgammon.egg-info/top_level.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ backgammon
2
+ tests
backgammon-1.0.0/backgammon/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from backgammon.backgammon import Backgammon
backgammon-1.0.0/backgammon/__main__.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import cmd
16
+ from typing import cast, List, Optional, Tuple
17
+
18
+ try:
19
+ import readline
20
+ except ImportError:
21
+ pass
22
+
23
+ from backgammon.backgammon import Backgammon, BackgammonError
24
+ from backgammon.match import GameState, Player
25
+
26
+
27
+ class BackgammonShell(cmd.Cmd):
28
+ intro: str = "Type 'help' or '?' to list commands."
29
+ prompt: str = "backgammon> "
30
+ game: Backgammon
31
+
32
+ def do_new(self, arg: str) -> None:
33
+ """Start a new game."""
34
+ die_1: int
35
+ die_2: int
36
+
37
+ if (
38
+ hasattr(self, "game")
39
+ and self.game.match.game_state is not GameState.NOT_STARTED
40
+ ):
41
+ print("Game already started.")
42
+ return
43
+
44
+ self.game = Backgammon()
45
+ die_1, die_2 = self.game.first_roll()
46
+ self.game.match.game_state = GameState.PLAYING
47
+
48
+ print(f"Rolled {die_1} {die_2}")
49
+ print(self.game)
50
+
51
+ def do_move(self, arg: str) -> None:
52
+ """Make a backgammon move: move <from> <to> ..."""
53
+ Moves = Tuple[Tuple[Optional[int], Optional[int]], ...]
54
+
55
+ def parse_arg(arg: str) -> Moves:
56
+ arg_ints: List[Optional[int]] = list(
57
+ map(lambda n: int(n) if n.isdigit() else None, arg.split())
58
+ )
59
+
60
+ if len(arg_ints) % 2 == 1:
61
+ raise ValueError("Incomplete move.")
62
+ if len(arg_ints) > 8:
63
+ raise ValueError("Too many moves.")
64
+
65
+ return cast(
66
+ Moves,
67
+ tuple(tuple(arg_ints[i : i + 2]) for i in range(0, len(arg_ints), 2)),
68
+ )
69
+
70
+ if (
71
+ not hasattr(self, "game")
72
+ or self.game.match.game_state is not GameState.PLAYING
73
+ ):
74
+ print("Game not in progress.")
75
+ return
76
+
77
+ try:
78
+ moves: Moves = parse_arg(arg)
79
+ except ValueError as e:
80
+ print(e)
81
+ return
82
+
83
+ try:
84
+ self.game.play(moves)
85
+ except BackgammonError:
86
+ print("Illegal move.")
87
+ return
88
+
89
+ if self.game.match.game_state is GameState.GAME_OVER:
90
+ print(f"P{1 if self.game.match.player is Player.ZERO else 2} wins")
91
+ print(self.game)
92
+ del self.game
93
+ else:
94
+ self.game.end_turn()
95
+
96
+ die_1, die_2 = self.game.roll()
97
+ print(f"Rolled {die_1} {die_2}")
98
+ print(self.game)
99
+
100
+ def do_quit(self, arg: str) -> bool:
101
+ """Exit the program."""
102
+ return True
103
+
104
+
105
+ if __name__ == "__main__":
106
+ try:
107
+ BackgammonShell().cmdloop()
108
+ except KeyboardInterrupt:
109
+ print("^C")
backgammon-1.0.0/backgammon/backgammon.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import enum
16
+ import itertools
17
+ import json
18
+ import operator
19
+ import random
20
+ from typing import Callable, List, NamedTuple, Optional, Tuple, Set
21
+
22
+ import backgammon.match
23
+ from backgammon.match import Player, GameState, Resign
24
+ import backgammon.position
25
+
26
+ MatchType = backgammon.match.Match
27
+ PositionType = backgammon.position.Position
28
+
29
+ STARTING_POSITION_ID = "4HPwATDgc/ABMA"
30
+ STARTING_MATCH_ID = "cAgAAAAAAAAA"
31
+
32
+ CHECKERS = 15
33
+ POINTS = 24
34
+ POINTS_PER_QUADRANT = int(POINTS / 4)
35
+
36
+ ASCII_BOARD_HEIGHT = 11
37
+ ASCII_MAX_CHECKERS = 5
38
+ ASCII_13_24 = "+13-14-15-16-17-18------19-20-21-22-23-24-+"
39
+ ASCII_12_01 = "+12-11-10--9--8--7-------6--5--4--3--2--1-+"
40
+
41
+
42
+ class BackgammonError(Exception):
43
+ pass
44
+
45
+
46
+ class MoveState(enum.Enum):
47
+ BEAR_OFF = enum.auto()
48
+ ENTER_FROM_BAR = enum.auto()
49
+ DEFAULT = enum.auto()
50
+
51
+
52
+ class Move(NamedTuple):
53
+ pips: int
54
+ source: Optional[int]
55
+ destination: Optional[int]
56
+
57
+
58
+ class Play(NamedTuple):
59
+ moves: Tuple[Move, ...]
60
+ position: PositionType
61
+
62
+
63
+ class Backgammon:
64
+ def __init__(
65
+ self, position_id: str = STARTING_POSITION_ID, match_id: str = STARTING_MATCH_ID
66
+ ):
67
+ self.position: PositionType = backgammon.position.decode(position_id)
68
+ self.match: MatchType = backgammon.match.decode(match_id)
69
+
70
+ def generate_plays(self) -> List[Play]:
71
+ """Generate and return legal plays."""
72
+
73
+ def generate(
74
+ position: PositionType,
75
+ dice: Tuple[int, ...],
76
+ die: int = 0,
77
+ moves: Tuple[Move, ...] = (),
78
+ plays: List[Play] = [],
79
+ ) -> List[Play]:
80
+ """Generate and return all plays."""
81
+ new_position: Optional[PositionType]
82
+ destination: Optional[int]
83
+ point: int
84
+ num_checkers: int
85
+ pips: int
86
+
87
+ if die < len(dice):
88
+ pips = dice[die]
89
+
90
+ if position.player_bar > 0:
91
+ new_position, destination = position.enter(pips)
92
+ if new_position:
93
+ generate(
94
+ new_position,
95
+ dice,
96
+ die + 1,
97
+ moves + (Move(pips, None, destination),),
98
+ plays,
99
+ )
100
+ elif sum(position.player_home()) + position.player_off == CHECKERS:
101
+ for point, num_checkers in enumerate(
102
+ position.board_points[:POINTS_PER_QUADRANT]
103
+ ):
104
+ new_position, destination = position.off(point, pips)
105
+ if new_position:
106
+ generate(
107
+ new_position,
108
+ dice,
109
+ die + 1,
110
+ moves + (Move(pips, point, destination),),
111
+ plays,
112
+ )
113
+ else:
114
+ for point, num_checkers in enumerate(position.board_points):
115
+ new_position, destination = position.move(point, pips)
116
+ if new_position:
117
+ generate(
118
+ new_position,
119
+ dice,
120
+ die + 1,
121
+ moves + (Move(pips, point, destination),),
122
+ plays,
123
+ )
124
+
125
+ if len(moves) > 0:
126
+ plays.append(Play(moves, position))
127
+
128
+ return plays
129
+
130
+ doubles: bool = self.match.dice[0] == self.match.dice[1]
131
+ dice: Tuple[int, ...] = self.match.dice * 2 if doubles else self.match.dice
132
+
133
+ plays: List[Play] = generate(self.position, dice)
134
+ if not doubles:
135
+ plays += generate(self.position, dice[::-1])
136
+
137
+ if plays:
138
+ max_moves: int = max(len(p.moves) for p in plays)
139
+ if max_moves == 1:
140
+ max_pips: int = max(dice)
141
+ higher_plays: List[Play] = list(
142
+ filter(lambda p: p.moves[0].pips == max_pips, plays)
143
+ )
144
+ if higher_plays:
145
+ plays = higher_plays
146
+ else:
147
+ plays = list(filter(lambda p: len(p.moves) == max_moves, plays))
148
+
149
+ key_func: Callable = lambda p: hash(p.position)
150
+ plays = sorted(plays, key=key_func)
151
+ plays = list(
152
+ map(
153
+ next,
154
+ map(operator.itemgetter(1), itertools.groupby(plays, key_func)),
155
+ )
156
+ )
157
+
158
+ return plays
159
+
160
+ def start(self, length: int = 3) -> "Backgammon":
161
+ self.match.game_state = GameState.PLAYING
162
+ self.match.length = length
163
+ self.first_roll()
164
+
165
+ return self
166
+
167
+ def roll(self) -> Tuple[int, int]:
168
+ if self.match.dice != (0, 0):
169
+ raise BackgammonError(f"Dice have already been rolled: {self.match.dice}")
170
+
171
+ self.match.dice = (
172
+ random.SystemRandom().randrange(1, 6),
173
+ random.SystemRandom().randrange(1, 6),
174
+ )
175
+ return self.match.dice
176
+
177
+ def first_roll(self) -> Tuple[int, int]:
178
+ while True:
179
+ self.match.dice = (
180
+ random.SystemRandom().randrange(1, 6),
181
+ random.SystemRandom().randrange(1, 6),
182
+ )
183
+ if self.match.dice[0] != self.match.dice[1]:
184
+ break
185
+ if self.match.dice[0] > self.match.dice[1]:
186
+ self.match.player = Player.ZERO
187
+ self.match.turn = Player.ZERO
188
+ else:
189
+ self.match.player = Player.ONE
190
+ self.match.turn = Player.ONE
191
+ return self.match.dice
192
+
193
+ def play(
194
+ self, moves: Tuple[Tuple[Optional[int], Optional[int]], ...]
195
+ ) -> "Backgammon":
196
+ """Excecute a play, a sequence of moves."""
197
+ new_position: PositionType = self.position
198
+ for source, destination in moves:
199
+ new_position = new_position.apply_move(source, destination)
200
+
201
+ legal_plays: List[Play] = self.generate_plays()
202
+
203
+ if new_position in [play.position for play in legal_plays]:
204
+ self.position = new_position
205
+
206
+ if self.position.player_off == CHECKERS:
207
+ multiplier: int = 1
208
+ if self.position.opponent_off == 0:
209
+ if (
210
+ self.position.opponent_bar > 0
211
+ or sum(self.position.board_points[:POINTS_PER_QUADRANT]) != 0
212
+ ):
213
+ multiplier = 3
214
+ else:
215
+ multiplier = 2
216
+ self.match.update_score(multiplier)
217
+ if self.match.game_state is GameState.PLAYING:
218
+ self.match.reset_cube()
219
+ self.position = backgammon.position.decode(STARTING_POSITION_ID)
220
+ self.first_roll()
221
+ else:
222
+ self.end_turn()
223
+
224
+ else:
225
+ position_id: str = self.position.encode()
226
+ match_id: str = self.match.encode()
227
+ raise BackgammonError(f"Invalid move: {position_id}:{match_id} {moves}")
228
+
229
+ return self
230
+
231
+ def double(self) -> "Backgammon":
232
+ if self.match.dice != (0, 0):
233
+ raise BackgammonError("Cannot double: dice have been rolled")
234
+ elif (
235
+ self.match.cube_holder is not Player.CENTERED
236
+ and self.match.cube_holder is not self.match.player
237
+ ):
238
+ raise BackgammonError("Cannot double: not cube holder")
239
+ elif self.match.double:
240
+ raise BackgammonError("Cannot double: already doubled")
241
+ elif (
242
+ self.match.player_0_score
243
+ if self.match.player is Player.ZERO
244
+ else self.match.player_1_score + self.match.cube_value >= self.match.length
245
+ ):
246
+ raise BackgammonError("Cannot double: dead cube")
247
+ elif self.match.crawford:
248
+ raise BackgammonError("Cannot double: crawford game")
249
+
250
+ self.match.double = True
251
+ self.match.swap_turn()
252
+
253
+ return self
254
+
255
+ def accept_double(self) -> "Backgammon":
256
+ if self.match.double:
257
+ self.match.double = False
258
+ self.match.cube_value *= 2
259
+ self.match.cube_holder = (
260
+ Player.ZERO if self.match.turn is Player.ZERO else Player.ONE
261
+ )
262
+ self.match.swap_turn()
263
+ else:
264
+ raise BackgammonError("Cannot accept double: double not offered")
265
+
266
+ return self
267
+
268
+ def reject_double(self) -> "Backgammon":
269
+ if self.match.double:
270
+ self.match.drop_cube()
271
+ if self.match.game_state is GameState.PLAYING:
272
+ self.match.reset_cube()
273
+ self.position = backgammon.position.decode(STARTING_POSITION_ID)
274
+ self.first_roll()
275
+ else:
276
+ raise BackgammonError("Cannot reject double: double not offered")
277
+
278
+ return self
279
+
280
+ def skip(self) -> "Backgammon":
281
+ num_plays: int = len(self.generate_plays())
282
+ if num_plays == 0:
283
+ self.end_turn()
284
+ else:
285
+ raise BackgammonError(f"Cannot skip turn: {num_plays} possible plays")
286
+
287
+ return self
288
+
289
+ def end_turn(self) -> "Backgammon":
290
+ self.position = self.position.swap_players()
291
+ self.match.swap_players()
292
+ self.match.reset_dice()
293
+
294
+ return self
295
+
296
+ def encode(self) -> str:
297
+ return f"{self.position.encode()}:{self.match.encode()}"
298
+
299
+ def __repr__(self):
300
+ position_id: str = self.position.encode()
301
+ match_id: str = self.match.encode()
302
+ return f"{__name__}.{self.__class__.__name__}('{position_id}', '{match_id}')"
303
+
304
+ def __str__(self):
305
+ def checkers(top: List[int], bottom: List[int]) -> List[List[str]]:
306
+ """Return an ASCII checker matrix."""
307
+ ascii_checkers: List[List[str]] = [
308
+ [" " for j in range(len(top))] for i in range(ASCII_BOARD_HEIGHT)
309
+ ]
310
+
311
+ for half in (top, bottom):
312
+ for col, num_checkers in enumerate(half):
313
+ row: int = 0 if half is top else len(ascii_checkers) - 1
314
+ for i in range(abs(num_checkers)):
315
+ if (
316
+ abs(num_checkers) > ASCII_MAX_CHECKERS
317
+ and i == ASCII_MAX_CHECKERS - 1
318
+ ):
319
+ ascii_checkers[row][col] = f" {abs(num_checkers)} "
320
+ break
321
+ ascii_checkers[row][col] = " O " if num_checkers > 0 else " X "
322
+ row += 1 if half is top else -1
323
+
324
+ return ascii_checkers
325
+
326
+ def split(position: List[int]) -> Tuple[List[int], List[int]]:
327
+ """Return a position split into top (Player.ZERO 12-1) and bottom (Player.ZERO 13-24) halves."""
328
+
329
+ def normalize(position: List[int]) -> List[int]:
330
+ """Return position for Player.ZERO"""
331
+ if self.match.player is Player.ONE:
332
+ position = list(map(lambda n: -n, position[::-1]))
333
+ return position
334
+
335
+ position = normalize(position)
336
+
337
+ half_len: int = int(len(position) / 2)
338
+ top: List[int] = position[:half_len][::-1]
339
+ bottom: List[int] = position[half_len:]
340
+
341
+ return top, bottom
342
+
343
+ points: List[List[str]] = checkers(*split(self.position.board_points))
344
+
345
+ bar: List[List[str]] = checkers(
346
+ *split(
347
+ [
348
+ self.position.player_bar,
349
+ -self.position.opponent_bar,
350
+ ]
351
+ )
352
+ )
353
+
354
+ ascii_board: str = ""
355
+ position_id: str = self.position.encode()
356
+ ascii_board += f" Position ID: {position_id}\n"
357
+ match_id: str = self.match.encode()
358
+ ascii_board += f" Match ID : {match_id}\n"
359
+ ascii_board += (
360
+ " "
361
+ + (ASCII_12_01 if self.match.player is Player.ZERO else ASCII_13_24)
362
+ + "\n"
363
+ )
364
+ for i in range(len(points)):
365
+ ascii_board += (
366
+ ("^|" if self.match.player is Player.ZERO else "v|")
367
+ if i == int(ASCII_BOARD_HEIGHT / 2)
368
+ else " |"
369
+ )
370
+ ascii_board += "".join(points[i][:POINTS_PER_QUADRANT])
371
+ ascii_board += "|"
372
+ ascii_board += "BAR" if i == int(ASCII_BOARD_HEIGHT / 2) else bar[i][0]
373
+ ascii_board += "|"
374
+ ascii_board += "".join(points[i][POINTS_PER_QUADRANT:])
375
+ ascii_board += "|"
376
+ ascii_board += "\n"
377
+ ascii_board += (
378
+ " "
379
+ + (ASCII_13_24 if self.match.player is Player.ZERO else ASCII_12_01)
380
+ + "\n"
381
+ )
382
+
383
+ return ascii_board
backgammon-1.0.0/backgammon/match.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import base64
16
+ import dataclasses
17
+ import enum
18
+ import math
19
+ import struct
20
+ from typing import Tuple
21
+
22
+
23
+ @enum.unique
24
+ class Player(enum.IntEnum):
25
+ ZERO = 0b00
26
+ ONE = 0b01
27
+ CENTERED = 0b11
28
+
29
+
30
+ @enum.unique
31
+ class GameState(enum.IntEnum):
32
+ NOT_STARTED = 0b000
33
+ PLAYING = 0b001
34
+ GAME_OVER = 0b010
35
+ RESIGNED = 0b011
36
+ DROPPED_CUBE = 0b100
37
+
38
+
39
+ @enum.unique
40
+ class Resign(enum.IntEnum):
41
+ NONE = 0b00
42
+ SINGLE_GAME = 0b01
43
+ GAMMON = 0b10
44
+ BACKGAMMON = 0b11
45
+
46
+
47
+ @dataclasses.dataclass
48
+ class Match:
49
+ cube_value: int
50
+ cube_holder: Player
51
+ player: Player
52
+ crawford: bool
53
+ game_state: GameState
54
+ turn: Player
55
+ double: bool
56
+ resign: Resign
57
+ dice: Tuple[int, int]
58
+ length: int
59
+ player_0_score: int
60
+ player_1_score: int
61
+
62
+ def swap_players(self) -> "Match":
63
+ self.player = self.turn = (
64
+ Player.ZERO if self.player is Player.ONE else Player.ONE
65
+ )
66
+
67
+ return self
68
+
69
+ def swap_turn(self) -> "Match":
70
+ self.turn = Player.ZERO if self.turn is Player.ONE else Player.ONE
71
+
72
+ return self
73
+
74
+ def reset_dice(self) -> "Match":
75
+ self.dice = (0, 0)
76
+
77
+ return self
78
+
79
+ def reset_cube(self) -> "Match":
80
+ self.cube_holder = Player.CENTERED
81
+ self.cube_value = 1
82
+
83
+ return self
84
+
85
+ def drop_cube(self) -> "Match":
86
+ if self.player is Player.ZERO:
87
+ self.player_0_score += self.cube_value
88
+ else:
89
+ self.player_1_score += self.cube_value
90
+
91
+ self.double = False
92
+
93
+ if self.player_0_score >= self.length or self.player_1_score >= self.length:
94
+ self.game_state = GameState.DROPPED_CUBE
95
+
96
+ return self
97
+
98
+ def update_score(self, multiplier: int) -> "Match":
99
+ points: int = self.cube_value * multiplier
100
+
101
+ self.crawford = False
102
+
103
+ if self.player is Player.ZERO:
104
+ self.player_0_score += points
105
+ if (
106
+ self.length - self.player_0_score == 1
107
+ and self.length - self.player_1_score > 1
108
+ ):
109
+ self.crawford = True
110
+ else:
111
+ self.player_1_score += points
112
+ if (
113
+ self.length - self.player_1_score == 1
114
+ and self.length - self.player_0_score > 1
115
+ ):
116
+ self.crawford = True
117
+
118
+ self.double = False
119
+
120
+ if self.player_0_score >= self.length or self.player_1_score >= self.length:
121
+ self.game_state = GameState.GAME_OVER
122
+
123
+ return self
124
+
125
+ def encode(self) -> str:
126
+ """Encode the match and return a match ID.
127
+
128
+ >>> match = Match(cube_value=2, cube_holder=Player.ZERO, player=Player.ONE, crawford=False, game_state=GameState.PLAYING, turn=Player.ONE, double=False, resign=Resign.NONE, dice=(5, 2), length=9, player_0_score=2, player_1_score=4)
129
+ >>> match.encode()
130
+ 'QYkqASAAIAAA'
131
+ """
132
+ match_key: str = "".join(
133
+ (
134
+ f"{int(math.log(self.cube_value, 2)):04b}"[::-1],
135
+ f"{self.cube_holder.value:02b}"[::-1],
136
+ f"{self.player.value:b}",
137
+ f"{self.crawford:b}",
138
+ f"{self.game_state.value:03b}"[::-1],
139
+ f"{self.turn:b}",
140
+ f"{self.double:b}",
141
+ f"{self.resign.value:02b}"[::-1],
142
+ f"{self.dice[0]:03b}"[::-1],
143
+ f"{self.dice[1]:03b}"[::-1],
144
+ f"{self.length:015b}"[::-1],
145
+ f"{self.player_0_score:015b}"[::-1],
146
+ f"{self.player_1_score:015b}"[::-1],
147
+ )
148
+ )
149
+ byte_strings: Tuple[str, ...] = tuple(
150
+ match_key[i : i + 8][::-1] for i in range(0, len(match_key), 8)
151
+ )
152
+ match_bytes: bytes = struct.pack("9B", *(int(b, 2) for b in byte_strings))
153
+ return base64.b64encode(bytes(match_bytes)).decode()
154
+
155
+
156
+ def decode(match_id: str) -> Match:
157
+ """Decode a match ID and return a Match.
158
+
159
+ >>> decode("QYkqASAAIAAA")
160
+ Match(cube_value=2, cube_holder=<Player.ZERO: 0>, player=<Player.ONE: 1>, crawford=False, game_state=<GameState.PLAYING: 1>, turn=<Player.ONE: 1>, double=False, resign=<Resign.NONE: 0>, dice=(5, 2), length=9, player_0_score=2, player_1_score=4)
161
+ """
162
+ match_bytes: bytes = base64.b64decode(match_id)
163
+ match_key: str = "".join([format(b, "08b")[::-1] for b in match_bytes])
164
+ return Match(
165
+ cube_value=2 ** int(match_key[0:4][::-1], 2),
166
+ cube_holder=Player(int(match_key[4:6][::-1], 2)),
167
+ player=Player(int(match_key[6])),
168
+ crawford=bool(int(match_key[7])),
169
+ game_state=GameState(int(match_key[8:11][::-1], 2)),
170
+ turn=Player(int(match_key[11])),
171
+ double=bool(int(match_key[12])),
172
+ resign=Resign(int(match_key[13:15][::-1], 2)),
173
+ dice=(int(match_key[15:18][::-1], 2), int(match_key[18:21][::-1], 2)),
174
+ length=int(match_key[21:36][::-1], 2),
175
+ player_0_score=int(match_key[36:51][::-1], 2),
176
+ player_1_score=int(match_key[51:66][::-1], 2),
177
+ )
backgammon-1.0.0/backgammon/position.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2020 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Position ID
16
+ # https://www.gnu.org/software/gnubg/manual/html_node/A-technical-description-of-the-Position-ID.html
17
+ # Official documentation is inaccurate. Position key (binary string) starts from the opponent's ace point. See:
18
+ # https://lists.gnu.org/archive/html/bug-gnubg/2005-01/msg00081.html
19
+ # https://lists.gnu.org/archive/html/bug-gnubg/2013-01/msg00010.html
20
+
21
+ import base64
22
+ import dataclasses
23
+ import struct
24
+ from typing import List, Optional, Tuple
25
+
26
+ POINTS = 24
27
+ POINTS_PER_QUADRANT = int(POINTS / 4)
28
+
29
+
30
+ @dataclasses.dataclass(frozen=True)
31
+ class Position:
32
+ board_points: Tuple[int, ...]
33
+ player_bar: int
34
+ player_off: int
35
+ opponent_bar: int
36
+ opponent_off: int
37
+
38
+ def enter(self, pips: int) -> Tuple[Optional["Position"], Optional[int]]:
39
+ """Try to enter from the bar and return the new position and destination."""
40
+ destination: int = POINTS - pips
41
+
42
+ if self.board_points[destination] >= -1:
43
+ return self.apply_move(None, destination), destination
44
+
45
+ return None, None
46
+
47
+ def player_home(self) -> Tuple[int, ...]:
48
+ """Return checkers in the player's home board."""
49
+ home_board: Tuple[int, ...] = self.board_points[:POINTS_PER_QUADRANT]
50
+
51
+ return tuple(point if point > 0 else 0 for point in home_board)
52
+
53
+ def off(self, point: int, pips: int) -> Tuple[Optional["Position"], Optional[int]]:
54
+ """Try to move a checker in the player's home board and return the new position and destination."""
55
+ if self.board_points[point] > 0:
56
+ destination: int = point - pips
57
+ if destination < 0:
58
+ checkers_on_higher_points: int = sum(
59
+ self.player_home()[point + 1 : POINTS_PER_QUADRANT]
60
+ )
61
+ if destination == -1 or checkers_on_higher_points == 0:
62
+ return self.apply_move(point, None), None
63
+ elif self.board_points[destination] >= -1:
64
+ return self.apply_move(point, destination), destination
65
+
66
+ return None, None
67
+
68
+ def move(self, point: int, pips: int) -> Tuple[Optional["Position"], Optional[int]]:
69
+ """Try to move a checker and return the new position and destination."""
70
+ if self.board_points[point] > 0:
71
+ destination: int = point - pips
72
+ if destination >= 0 and self.board_points[destination] >= -1:
73
+ return self.apply_move(point, destination), destination
74
+
75
+ return None, None
76
+
77
+ def apply_move(
78
+ self, source: Optional[int], destination: Optional[int]
79
+ ) -> "Position":
80
+ """Apply a move and return a new position."""
81
+ board_points: List[int] = list(self.board_points)
82
+ player_bar: int = self.player_bar
83
+ player_off: int = self.player_off
84
+ opponent_bar: int = self.opponent_bar
85
+ opponent_off: int = self.opponent_off
86
+
87
+ if source is None:
88
+ player_bar -= 1
89
+ else:
90
+ board_points[source] -= 1
91
+
92
+ if destination is None:
93
+ player_off += 1
94
+ else:
95
+ hit: bool = True if board_points[destination] == -1 else False
96
+ if hit:
97
+ board_points[destination] = 1
98
+ opponent_bar += 1
99
+ else:
100
+ board_points[destination] += 1
101
+
102
+ return Position(
103
+ tuple(board_points), player_bar, player_off, opponent_bar, opponent_off
104
+ )
105
+
106
+ def swap_players(self) -> "Position":
107
+ return Position(
108
+ tuple(map(lambda n: -n, self.board_points[::-1])),
109
+ self.opponent_bar,
110
+ self.opponent_off,
111
+ self.player_bar,
112
+ self.player_off,
113
+ )
114
+
115
+ def encode(self) -> str:
116
+ """Encode the position and return a position ID.
117
+
118
+ >>> position = Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2), player_bar=0, player_off=0, opponent_bar=0, opponent_off=0)
119
+ >>> position.encode()
120
+ '4HPwATDgc/ABMA'
121
+ """
122
+ player_points, opponent_points = _unmerge_points(self.board_points)
123
+ checkers: Tuple[int, ...] = (
124
+ opponent_points + (self.opponent_bar,) + player_points + (self.player_bar,)
125
+ )
126
+
127
+ position_key: str = _key_from_checkers(checkers)
128
+
129
+ position_id: str = _id_from_key(position_key)
130
+
131
+ return position_id
132
+
133
+
134
+ def _unmerge_points(
135
+ board_points: Tuple[int, ...]
136
+ ) -> Tuple[Tuple[int, ...], Tuple[int, ...]]:
137
+ """Return player and opponent board positions starting from their respective ace points."""
138
+ player: Tuple[int, ...] = tuple(
139
+ map(
140
+ lambda n: 0 if n < 0 else n,
141
+ board_points,
142
+ )
143
+ )
144
+ opponent: Tuple[int, ...] = tuple(
145
+ map(
146
+ lambda n: 0 if n > 0 else -n,
147
+ board_points[::-1],
148
+ )
149
+ )
150
+ return player, opponent
151
+
152
+
153
+ def _key_from_checkers(checkers: Tuple[int, ...]) -> str:
154
+ """Return a position key (bit string)."""
155
+ return "".join("1" * n + "0" for n in checkers).ljust(80, "0")
156
+
157
+
158
+ def _id_from_key(position_key: str) -> str:
159
+ """Encode the position key and return the ID."""
160
+ byte_strings: Tuple[str, ...] = tuple(
161
+ position_key[i : i + 8][::-1] for i in range(0, len(position_key), 8)
162
+ )
163
+ position_bytes: bytes = struct.pack("10B", *(int(b, 2) for b in byte_strings))
164
+ return base64.b64encode(position_bytes).decode()[:-2]
165
+
166
+
167
+ def decode(position_id: str) -> Position:
168
+ """Decode a position ID and return a Position.
169
+
170
+ >>> decode('4HPwATDgc/ABMA')
171
+ Position(board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2), player_bar=0, player_off=0, opponent_bar=0, opponent_off=0)
172
+ """
173
+ position_key: str = _key_from_id(position_id)
174
+
175
+ checkers: Tuple[int, ...] = _checkers_from_key(position_key)
176
+
177
+ player_points: Tuple[int, ...] = checkers[25:49]
178
+ opponent_points: Tuple[int, ...] = checkers[:24]
179
+ board_points: Tuple[int, ...] = _merge_points(player_points, opponent_points)
180
+
181
+ player_bar: int = checkers[49]
182
+ player_off: int = abs(15 - sum(player_points) - player_bar)
183
+
184
+ opponent_bar: int = checkers[24]
185
+ opponent_off: int = abs(15 - sum(opponent_points) - abs(opponent_bar))
186
+
187
+ return Position(
188
+ board_points=board_points,
189
+ player_bar=player_bar,
190
+ player_off=player_off,
191
+ opponent_bar=opponent_bar,
192
+ opponent_off=opponent_off,
193
+ )
194
+
195
+
196
+ def _key_from_id(position_id: str) -> str:
197
+ """Decode the the position ID and return the key (bit string)."""
198
+ position_bytes: bytes = base64.b64decode(position_id + "==")
199
+ position_key: str = "".join([format(b, "08b")[::-1] for b in position_bytes])
200
+ return position_key
201
+
202
+
203
+ def _checkers_from_key(position_key: str) -> Tuple[int, ...]:
204
+ """Return a list of checkers."""
205
+ return tuple(sum(int(n) for n in pos) for pos in position_key.split("0")[:50])
206
+
207
+
208
+ def _merge_points(
209
+ player: Tuple[int, ...], opponent: Tuple[int, ...]
210
+ ) -> Tuple[int, ...]:
211
+ """Merge player and opponent board positions and return the combined points."""
212
+ return tuple(
213
+ i + j for i, j in zip(player, tuple(map(lambda n: -n, opponent[::-1])))
214
+ )
backgammon-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
backgammon-1.0.0/setup.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2019 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from setuptools import setup, find_packages
16
+
17
+ with open("README.rst", encoding="utf-8") as f:
18
+ long_description = f.read()
19
+
20
+ setup(
21
+ name="backgammon",
22
+ version="1.0.0",
23
+ author="Softwerks",
24
+ author_email="info@softwerks.com",
25
+ description="Backgammon engine for the Backgammon Network.",
26
+ long_description=long_description,
27
+ long_description_content_type="text/x-rst",
28
+ url="https://github.com/softwerks/backgammon",
29
+ packages=find_packages(),
30
+ classifiers=[
31
+ "Programming Language :: Python :: 3",
32
+ "License :: OSI Approved :: Apache Software License",
33
+ "Operating System :: OS Independent",
34
+ ],
35
+ python_requires=">=3.7",
36
+ )
backgammon-1.0.0/tests/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
backgammon-1.0.0/tests/test_backgammon.py ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import unittest
16
+ from unittest import mock
17
+
18
+ from backgammon import backgammon
19
+ from backgammon.backgammon import Play, Move
20
+ from backgammon.position import Position
21
+
22
+
23
+ class TestBackgammon(unittest.TestCase):
24
+ # fmt: off
25
+ def test_enter(self):
26
+ # -----19-20-21-22-23-24-+
27
+ # | | O O O O O O |
28
+ # | | O O O O O O |
29
+ # | | |
30
+ # |BAR| (5, 4) |
31
+ # | X | |
32
+ self.assertEqual(
33
+ backgammon.Backgammon("27YBAAAAACAAAA", "cInyAAAAAAAE").generate_plays(),
34
+ [],
35
+ )
36
+
37
+ # -----19-20-21-22-23-24-+
38
+ # | | O O O O O |
39
+ # | | O O O O |
40
+ # | | |
41
+ # |BAR| (5, 4) |
42
+ # | X | |
43
+ self.assertEqual(
44
+ backgammon.Backgammon("2zIAAAAAAAQAAA", "cInyAAAAAAAE").generate_plays(),
45
+ [
46
+ Play(
47
+ moves=(
48
+ Move(pips=4, source=None, destination=20),
49
+ Move(pips=5, source=20, destination=15),
50
+ ),
51
+ position=Position(
52
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -2, 0, 0, -2, -2, -2),
53
+ player_bar=0,
54
+ player_off=14,
55
+ opponent_bar=1,
56
+ opponent_off=6,
57
+ ),
58
+ ),
59
+ Play(
60
+ moves=(
61
+ Move(pips=5, source=None, destination=19),
62
+ Move(pips=4, source=19, destination=15),
63
+ ),
64
+ position=Position(
65
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, -2, 0, -1, -2, -2, -2),
66
+ player_bar=0,
67
+ player_off=14,
68
+ opponent_bar=0,
69
+ opponent_off=6,
70
+ ),
71
+ ),
72
+ ],
73
+ )
74
+
75
+ def test_bear_off(self):
76
+ # | (4, 3)
77
+ # | X X O |
78
+ # | X X O |
79
+ # --6--5--4--3--2--1-+
80
+ self.assertEqual(
81
+ backgammon.Backgammon("AACAYQMAAAAAAA", "cAnuAAAAAAAE").generate_plays(),
82
+ [
83
+ Play(
84
+ moves=(
85
+ Move(pips=4, source=3, destination=None),
86
+ Move(pips=3, source=2, destination=None),
87
+ ),
88
+ position=Position(
89
+ board_points=(-2, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
90
+ player_bar=0,
91
+ player_off=13,
92
+ opponent_bar=0,
93
+ opponent_off=13,
94
+ ),
95
+ )
96
+ ],
97
+ )
98
+
99
+ # | (4, 2)
100
+ # | X O |
101
+ # | X X O |
102
+ # --6--5--4--3--2--1-+
103
+ self.assertEqual(
104
+ backgammon.Backgammon("AACAMQEAAAAAAA", "cAnqAAAAAAAE").generate_plays(),
105
+ [
106
+ Play(
107
+ moves=(
108
+ Move(pips=2, source=3, destination=1),
109
+ Move(pips=4, source=1, destination=None),
110
+ ),
111
+ position=Position(
112
+ board_points=(-2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
113
+ player_bar=0,
114
+ player_off=13,
115
+ opponent_bar=0,
116
+ opponent_off=13,
117
+ ),
118
+ ),
119
+ Play(
120
+ moves=(
121
+ Move(pips=4, source=3, destination=None),
122
+ Move(pips=2, source=1, destination=None),
123
+ ),
124
+ position=Position(
125
+ board_points=(-2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
126
+ player_bar=0,
127
+ player_off=14,
128
+ opponent_bar=0,
129
+ opponent_off=13,
130
+ ),
131
+ ),
132
+ ],
133
+ )
134
+
135
+ # | (6, 4)
136
+ # | X O |
137
+ # | X X O |
138
+ # --6--5--4--3--2--1-+
139
+ self.assertEqual(
140
+ backgammon.Backgammon("AACAIQMAAAAAAA", "cAnzAAAAAAAE").generate_plays(),
141
+ [
142
+ Play(
143
+ moves=(
144
+ Move(pips=6, source=4, destination=None),
145
+ ),
146
+ position=Position(
147
+ board_points=(-2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
148
+ player_bar=0,
149
+ player_off=13,
150
+ opponent_bar=0,
151
+ opponent_off=13,
152
+ ),
153
+ )
154
+ ],
155
+ )
156
+
157
+ # | (6, 4)
158
+ # | X O |
159
+ # | X X X O |
160
+ # --6--5--4--3--2--1-+
161
+ self.assertEqual(
162
+ backgammon.Backgammon("AACAIQsAAAAAAA", "cAnzAAAAAAAE").generate_plays(),
163
+ [
164
+ Play(
165
+ moves=(
166
+ Move(pips=4, source=5, destination=1),
167
+ Move(pips=6, source=4, destination=None),
168
+ ),
169
+ position=Position(
170
+ board_points=(-2, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
171
+ player_bar=0,
172
+ player_off=12,
173
+ opponent_bar=0,
174
+ opponent_off=13,
175
+ ),
176
+ )
177
+ ],
178
+ )
179
+
180
+ def test_move(self):
181
+ # +13-14-15-16-17-18------19-20-21-22-23-24-+
182
+ # | O O | | O O O O X |
183
+ # | O O | | O O O O |
184
+ # | O | | O O |
185
+ # | | | |
186
+ # | | | |
187
+ # | |BAR| (6, 6) |
188
+ # | | | X |
189
+ # | | | X |
190
+ # | | | X X |
191
+ # | | | X X X X X |
192
+ # | | | X X X X X |
193
+ # +12-11-10--9--8--7-------6--5--4--3--2--1-+
194
+ self.assertEqual(
195
+ backgammon.Backgammon("bHc3AADfbQMAIA", "cAn7AAAAAAAE").generate_plays(),
196
+ [],
197
+ )
198
+
199
+ # -19-20-21-22-23-24-+
200
+ # | O O X X X |
201
+ # | O X |
202
+ # | X |
203
+ # | (1, 1) |
204
+ self.assertEqual(
205
+ backgammon.Backgammon("mAAAAAAArgAAAA", "cInkAAAAAAAE").generate_plays(),
206
+ [
207
+ Play(
208
+ moves=(
209
+ Move(pips=1, source=22, destination=21),
210
+ Move(pips=1, source=23, destination=22),
211
+ Move(pips=1, source=22, destination=21),
212
+ ),
213
+ position=Position(
214
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -2, 5, 0, 0),
215
+ player_bar=0,
216
+ player_off=10,
217
+ opponent_bar=0,
218
+ opponent_off=12,
219
+ ),
220
+ )
221
+ ],
222
+ )
223
+
224
+ # +13-14-15-16-17-18------19-20-21-22-23-24-+
225
+ # | O O O O X | | O O O X |
226
+ # | O O O | | O O |
227
+ # | O | | O O |
228
+ # | | | (3, 2) |
229
+ self.assertEqual(
230
+ backgammon.Backgammon("rsPOAgAAAAIBAA", "cImpAAAAAAAE").generate_plays(),
231
+ [
232
+ Play(
233
+ moves=(
234
+ Move(pips=3, source=17, destination=14),
235
+ Move(pips=2, source=14, destination=12),
236
+ ),
237
+ position=Position(
238
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, -3, -2, 0, 0, 0, -3, -1, -3, 1),
239
+ player_bar=0,
240
+ player_off=13,
241
+ opponent_bar=1,
242
+ opponent_off=0,
243
+ ),
244
+ ),
245
+ Play(
246
+ moves=(
247
+ Move(pips=3, source=17, destination=14),
248
+ Move(pips=2, source=23, destination=21),
249
+ ),
250
+ position=Position(
251
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, 1, -3, -2, 0, 0, 0, -3, 1, -3, 0),
252
+ player_bar=0,
253
+ player_off=13,
254
+ opponent_bar=1,
255
+ opponent_off=0,
256
+ ),
257
+ ),
258
+ Play(
259
+ moves=(
260
+ Move(pips=2, source=23, destination=21),
261
+ Move(pips=3, source=21, destination=18),
262
+ ),
263
+ position=Position(
264
+ board_points=(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -2, 0, -3, -2, 1, 1, 0, -3, 0, -3, 0),
265
+ player_bar=0,
266
+ player_off=13,
267
+ opponent_bar=1,
268
+ opponent_off=0,
269
+ ),
270
+ ),
271
+ ],
272
+ )
273
+ # fmt: on
274
+
275
+ @mock.patch("random.SystemRandom.randrange", side_effect=[3, 4])
276
+ def test_start(self, randrange_mock):
277
+ self.assertEqual(
278
+ backgammon.Backgammon().start().encode(), "4HPwATDgc/ABMA:cIlxAAAAAAAA"
279
+ )
280
+
281
+ @mock.patch("random.SystemRandom.randrange", side_effect=[3, 4])
282
+ def test_roll(self, randrange_mock):
283
+ with self.assertRaises(backgammon.BackgammonError):
284
+ backgammon.Backgammon("4HPwATDgc/ABMA", "MAAOAAAAAAAA").roll()
285
+
286
+ self.assertEqual(backgammon.Backgammon().roll(), (3, 4))
287
+
288
+ @mock.patch("random.SystemRandom.randrange", side_effect=[3, 3, 4, 3, 3, 4])
289
+ def test_first_roll(self, randrange_mock):
290
+ p0: backgammon.Backgammon = backgammon.Backgammon()
291
+ self.assertEqual(p0.first_roll(), (4, 3))
292
+ self.assertEqual(p0.encode(), "4HPwATDgc/ABMA:MAAOAAAAAAAA")
293
+
294
+ p1: backgammon.Backgammon = backgammon.Backgammon()
295
+ self.assertEqual(p1.first_roll(), (3, 4))
296
+ self.assertEqual(p1.encode(), "4HPwATDgc/ABMA:cIgRAAAAAAAA")
297
+
298
+ @mock.patch("random.SystemRandom.randrange", side_effect=[5, 4, 3, 4, 4, 1])
299
+ def test_play(self, randrange_mock):
300
+ self.assertEqual(
301
+ backgammon.Backgammon("4NvBEQiYz+ABAw", "cAlqAAAAAAAE")
302
+ .play(((12, 8), (12, 10)))
303
+ .encode(),
304
+ "mM+SAQPg28EBRA:MAFgAAAAAAAA",
305
+ )
306
+
307
+ self.assertEqual(
308
+ backgammon.Backgammon("CwAAiAIAAAAAAA", "cAluAAAAAAAE")
309
+ .play(((3, None), (2, None)))
310
+ .encode(),
311
+ "4HPwATDgc/ABMA:MIFyAAAACAAA",
312
+ )
313
+
314
+ self.assertEqual(
315
+ backgammon.Backgammon("2+4FAEAhAAAAAA", "cAlvAAAAAAAE")
316
+ .play(((4, None), (0, None)))
317
+ .encode(),
318
+ "2+4FAEAAAAAAAA:cApvAAAAGAAA",
319
+ )
320
+
321
+ self.assertEqual(
322
+ backgammon.Backgammon("2+4FAAQhAAAAAA", "cAlvAAAAAAAE")
323
+ .play(((4, None), (0, None)))
324
+ .encode(),
325
+ "2+4FAAQAAAAAAA:cApvAAAAGAAA",
326
+ )
327
+
328
+ self.assertEqual(
329
+ backgammon.Backgammon("2+4NAAAhAAAAAA", "cAlvAAAAAAAE")
330
+ .play(((4, None), (0, None)))
331
+ .encode(),
332
+ "4HPwATDgc/ABMA:8IlxAAAAEAAA",
333
+ )
334
+
335
+ self.assertEqual(
336
+ backgammon.Backgammon("XwAAgAEAAAAAAA", "cIltACAAAAAA")
337
+ .play(((0, None), (0, None)))
338
+ .encode(),
339
+ "4HPwATDgc/ABMA:MAFmACAACAAA",
340
+ )
341
+
342
+ with self.assertRaises(backgammon.BackgammonError):
343
+ backgammon.Backgammon("4HPwATDgc/ABMA", "cAlqAAAAAAAE").play(
344
+ ((12, 8), (19, 17))
345
+ )
346
+
347
+ def test_double(self):
348
+ with self.assertRaises(backgammon.BackgammonError):
349
+ backgammon.Backgammon("4HPwATDgc/ABMA", "cInxABAAAAAA").double()
350
+
351
+ with self.assertRaises(backgammon.BackgammonError):
352
+ backgammon.Backgammon("4HPhASLgc/ABMA", "EQHgAAAAAAAA").double()
353
+
354
+ with self.assertRaises(backgammon.BackgammonError):
355
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MBngAAAAAAAE").double()
356
+
357
+ with self.assertRaises(backgammon.BackgammonError):
358
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MAHgAGAAKAAA").double()
359
+
360
+ with self.assertRaises(backgammon.BackgammonError):
361
+ backgammon.Backgammon("4HPhASLgc/ABMA", "8AmgAEAAGAAA").double()
362
+
363
+ self.assertEqual(
364
+ backgammon.Backgammon("0PPgATDgc+EBIg", "UQngAAAAAAAA").double().encode(),
365
+ "0PPgATDgc+EBIg:URHgAAAAAAAA",
366
+ )
367
+
368
+ def test_accept_double(self):
369
+ self.assertEqual(
370
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MBngAAAAAAAE")
371
+ .accept_double()
372
+ .encode(),
373
+ "4HPhASLgc/ABMA:EQHgAAAAAAAA",
374
+ )
375
+
376
+ with self.assertRaises(backgammon.BackgammonError):
377
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MAHgAAAAAAAA").accept_double()
378
+
379
+ @mock.patch("random.SystemRandom.randrange", side_effect=[3, 4])
380
+ def test_reject_double(self, randrange_mock):
381
+ self.assertEqual(
382
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MBngAAAAAAAE")
383
+ .reject_double()
384
+ .encode(),
385
+ "4HPwATDgc/ABMA:cInxABAAAAAA",
386
+ )
387
+
388
+ with self.assertRaises(backgammon.BackgammonError):
389
+ backgammon.Backgammon("4HPhASLgc/ABMA", "MAHgAAAAAAAA").reject_double()
390
+
391
+ def test_skip(self):
392
+ self.assertEqual(
393
+ backgammon.Backgammon("ZgAAAAAAIAAAAA", "cAnqAAAAAAAE").skip().encode(),
394
+ "AAAAmQEAAAAAAA:MAHgAAAAAAAA",
395
+ )
396
+
397
+ with self.assertRaises(backgammon.BackgammonError):
398
+ backgammon.Backgammon("MgAAAAAAEAAAAA", "cAnqAAAAAAAE").skip()
399
+
400
+ def test_end_turn(self):
401
+ self.assertEqual(
402
+ backgammon.Backgammon("ywEXAIGDAQEMAA", "MIGxABAAEAAA").end_turn().encode(),
403
+ "OBgQwJYDLgACAA:cAmgABAAEAAA",
404
+ )
405
+
406
+ def test_encode(self):
407
+ self.assertEqual(
408
+ backgammon.Backgammon("4HPwATDgc/ABMA", "cAgAAAAAAAAA").encode(),
409
+ "4HPwATDgc/ABMA:cAgAAAAAAAAA",
410
+ )
411
+
412
+
413
+ if __name__ == "__main__":
414
+ unittest.main()
backgammon-1.0.0/tests/test_match.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import unittest
16
+
17
+ from backgammon import match
18
+
19
+
20
+ class TestMatch(unittest.TestCase):
21
+ def test_swap_players(self):
22
+ self.assertEqual(
23
+ match.decode("cImxABAAEAAA").swap_players(), match.decode("MIGxABAAEAAA")
24
+ )
25
+
26
+ def test_swap_turn(self):
27
+ self.assertEqual(
28
+ match.decode("cImxABAAEAAA").swap_turn(), match.decode("cIGxABAAEAAA")
29
+ )
30
+
31
+ def test_reset_dice(self):
32
+ self.assertEqual(
33
+ match.decode("cImxABAAEAAA").reset_dice(), match.decode("cAmgABAAEAAA")
34
+ )
35
+
36
+ def test_reset_cube(self):
37
+ self.assertEqual(
38
+ match.decode("QYmxABAAEAAA").reset_cube(), match.decode("cImxABAAEAAA")
39
+ )
40
+
41
+ def test_drop_cube(self):
42
+ self.assertEqual(
43
+ match.decode("cBFgABAACAAA").drop_cube(), match.decode("cAFgABAAEAAA")
44
+ )
45
+ self.assertEqual(
46
+ match.decode("ARlgABAAEAAA").drop_cube(), match.decode("AQxgADAAEAAA")
47
+ )
48
+
49
+ def test_update_score(self):
50
+ self.assertEqual(
51
+ match.decode("QYkqASAAIAAA").update_score(1), match.decode("QYkqASAAMAAA")
52
+ )
53
+ self.assertEqual(
54
+ match.decode("MIFlABAAEAAA").update_score(3), match.decode("MIJlAEAAEAAA")
55
+ )
56
+ self.assertEqual(
57
+ match.decode("MIGqACAAGAAA").update_score(2), match.decode("sIGqAEAAGAAA")
58
+ )
59
+ self.assertEqual(
60
+ match.decode("QYnqACAAIAAA").update_score(1), match.decode("wYnqACAAMAAA")
61
+ )
62
+ self.assertEqual(
63
+ match.decode("MIGqACAAIAAA").update_score(2), match.decode("MIGqAEAAIAAA")
64
+ )
65
+
66
+ def test_encode(self):
67
+ self.assertEqual(
68
+ match.Match(
69
+ cube_value=2,
70
+ cube_holder=match.Player.ZERO,
71
+ player=match.Player.ONE,
72
+ crawford=False,
73
+ game_state=match.GameState.PLAYING,
74
+ turn=match.Player.ONE,
75
+ double=False,
76
+ resign=match.Resign.NONE,
77
+ dice=(5, 2),
78
+ length=9,
79
+ player_0_score=2,
80
+ player_1_score=4,
81
+ ).encode(),
82
+ "QYkqASAAIAAA",
83
+ )
84
+
85
+ def test_decode(self):
86
+ self.assertEqual(
87
+ match.decode("QYkqASAAIAAA"),
88
+ match.Match(
89
+ cube_value=2,
90
+ cube_holder=match.Player.ZERO,
91
+ player=match.Player.ONE,
92
+ crawford=False,
93
+ game_state=match.GameState.PLAYING,
94
+ turn=match.Player.ONE,
95
+ double=False,
96
+ resign=match.Resign.NONE,
97
+ dice=(5, 2),
98
+ length=9,
99
+ player_0_score=2,
100
+ player_1_score=4,
101
+ ),
102
+ )
103
+
104
+
105
+ if __name__ == "__main__":
106
+ unittest.main()
backgammon-1.0.0/tests/test_position.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2021 Softwerks LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import Tuple
16
+ import unittest
17
+
18
+ from backgammon import position
19
+
20
+
21
+ class TestPosition(unittest.TestCase):
22
+ def test_enter(self):
23
+ # ----19-20-21-22-23-24-+
24
+ # |BAR| O O O O O |
25
+ # | X | O O O O O |
26
+ pos: position.Position = position.decode("m20AAAAAAAgAAA")
27
+ self.assertEqual(pos.enter(3), (position.decode("m20AAAAAAAEAAA"), 21))
28
+ self.assertEqual(pos.enter(2), (None, None))
29
+
30
+ def test_player_home(self):
31
+ # | X |
32
+ # | X X O |
33
+ # | X X O X O |
34
+ # --6--5--4--3--2--1-+
35
+ pos: position.Position = position.decode("AAAQ41gAAAAAAA")
36
+ self.assertEqual(pos.player_home(), (0, 3, 0, 0, 2, 1))
37
+
38
+ def test_off(self):
39
+ # | X O |
40
+ # | X X X O |
41
+ # --6--5--4--3--2--1-+
42
+ pos: position.Position = position.decode("AACAMQUAAAAAAA")
43
+ self.assertEqual(pos.off(1, 2), (position.decode("AACAkQIAAAAAAA"), None))
44
+ self.assertEqual(pos.off(3, 4), (position.decode("AACAMQIAAAAAAA"), None))
45
+ self.assertEqual(pos.off(4, 6), (position.decode("AACAMQEAAAAAAA"), None))
46
+ self.assertEqual(pos.off(3, 2), (position.decode("AACAcQQAAAAAAA"), 1))
47
+ self.assertEqual(pos.off(3, 6), (None, None))
48
+
49
+ def test_move(self):
50
+ # -19-20-21-22-23-24-+
51
+ # | O O O O X |
52
+ # | O O O |
53
+ pos: position.Position = position.decode("tgwAAAAAgAAAAA")
54
+ self.assertEqual(pos.move(23, 3), (position.decode("NgYAQAAAEAAAAA"), 20))
55
+ self.assertEqual(pos.move(23, 4), (position.decode("tgwAAAAACAAAAA"), 19))
56
+ self.assertEqual(pos.move(23, 5), (None, None))
57
+
58
+ def test_apply_move(self):
59
+ # -----19-20-21-22-23-24-+
60
+ # |BAR| X O O |
61
+ # | X | O |
62
+ pos: position.Position = position.decode("CwAAAACAIAAAAA")
63
+ self.assertEqual(pos.apply_move(None, 22), position.decode("AwAABACACAAAAA"))
64
+ self.assertEqual(pos.apply_move(None, 21), position.decode("CwAAAACABAAAAA"))
65
+ self.assertEqual(pos.apply_move(None, 19), position.decode("CwAAAACAAQAAAA"))
66
+ # | X O |
67
+ # | X O O X |
68
+ # --6--5--4--3--2--1-+
69
+ pos: position.Position = position.decode("AABgEQwAAAAAAA")
70
+ self.assertEqual(pos.apply_move(5, 3), position.decode("AABgEQkAAAAAAA"))
71
+ self.assertEqual(pos.apply_move(5, 1), position.decode("AABgVAgAAAAAAA"))
72
+ self.assertEqual(pos.apply_move(5, 0), position.decode("AABgMQgAAAAAAA"))
73
+ self.assertEqual(pos.apply_move(5, None), position.decode("AABgEQQAAAAAAA"))
74
+
75
+ def test_swap_players(self):
76
+ # +13-14-15-16-17-18------19-20-21-22-23-24-+
77
+ # | O O X | | O O O |
78
+ # | O | | O O |
79
+ # | O | | O |
80
+ # | | | |
81
+ # | |BAR| X |
82
+ # | X | X | X |
83
+ # | X | X | X O |
84
+ # +12-11-10--9--8--7-------6--5--4--3--2--1-+
85
+ pos: position.Position = position.decode("ywEXAIGDAQEMAA")
86
+ self.assertEqual(pos.swap_players(), position.decode("OBgQwJYDLgACAA"))
87
+
88
+ # fmt: off
89
+
90
+ def test_encode(self):
91
+ pos: position.Position = position.Position(
92
+ board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2),
93
+ player_bar=0,
94
+ player_off=0,
95
+ opponent_bar=0,
96
+ opponent_off=0,
97
+ )
98
+ self.assertEqual(pos.encode(), "4HPwATDgc/ABMA")
99
+
100
+ def test_unmerge_points(self):
101
+ player: Tuple[int, ...]
102
+ opponent: Tuple[int, ...]
103
+
104
+ board_points: Tuple[int, ...] = (-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2)
105
+ player, opponent = position._unmerge_points(board_points)
106
+
107
+ self.assertEqual(
108
+ player,
109
+ (0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2),
110
+ )
111
+ self.assertEqual(
112
+ opponent,
113
+ (0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2),
114
+ )
115
+
116
+ def test_key_from_checkers(self):
117
+ unmerged_points: Tuple[int, ...] = (0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2)
118
+ bar: Tuple[int, ...] = (0,)
119
+
120
+ self.assertEqual(
121
+ position._key_from_checkers(unmerged_points + bar + unmerged_points + bar),
122
+ "00000111110011100000111110000000000011000000011111001110000011111000000000001100",
123
+ )
124
+
125
+ def test_id_from_key(self):
126
+ self.assertEqual(
127
+ position._id_from_key(
128
+ "00000111110011100000111110000000000011000000011111001110000011111000000000001100"
129
+ ),
130
+ "4HPwATDgc/ABMA",
131
+ )
132
+
133
+ def test_decode(self):
134
+ self.assertEqual(
135
+ position.decode("4HPwATDgc/ABMA"),
136
+ position.Position(
137
+ board_points=(-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2),
138
+ player_bar=0,
139
+ player_off=0,
140
+ opponent_bar=0,
141
+ opponent_off=0,
142
+ ),
143
+ )
144
+
145
+ def test_key_from_id(self):
146
+ self.assertEqual(
147
+ position._key_from_id("4HPwATDgc/ABMA"),
148
+ "00000111110011100000111110000000000011000000011111001110000011111000000000001100",
149
+ )
150
+
151
+ def test_checkers_from_key(self):
152
+ self.assertEqual(
153
+ position._checkers_from_key(
154
+ "00000111110011100000111110000000000011000000011111001110000011111000000000001100"
155
+ ),
156
+ (
157
+ 0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
158
+ 0,
159
+ 0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
160
+ 0,
161
+ ),
162
+ )
163
+
164
+ def test_merge_points(self):
165
+ self.assertEqual(
166
+ position._merge_points(
167
+ (0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2),
168
+ (0, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2),
169
+ ),
170
+ (-2, 0, 0, 0, 0, 5, 0, 3, 0, 0, 0, -5, 5, 0, 0, 0, -3, 0, -5, 0, 0, 0, 0, 2),
171
+ )
172
+
173
+ # fmt: on
174
+
175
+
176
+ if __name__ == "__main__":
177
+ unittest.main()