magmacrunch-backgammon 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- magmacrunch_backgammon-0.1.0/.gitignore +7 -0
- magmacrunch_backgammon-0.1.0/LICENSE +75 -0
- magmacrunch_backgammon-0.1.0/NOTICE +30 -0
- magmacrunch_backgammon-0.1.0/PKG-INFO +190 -0
- magmacrunch_backgammon-0.1.0/README.md +170 -0
- magmacrunch_backgammon-0.1.0/backgammon/__init__.py +0 -0
- magmacrunch_backgammon-0.1.0/backgammon/__main__.py +31 -0
- magmacrunch_backgammon-0.1.0/backgammon/app.py +73 -0
- magmacrunch_backgammon-0.1.0/backgammon/arcade.py +39 -0
- magmacrunch_backgammon-0.1.0/backgammon/online.py +382 -0
- magmacrunch_backgammon-0.1.0/backgammon/online_scenes.py +398 -0
- magmacrunch_backgammon-0.1.0/backgammon/rules.py +495 -0
- magmacrunch_backgammon-0.1.0/backgammon/scenes.py +709 -0
- magmacrunch_backgammon-0.1.0/backgammon/theme.py +70 -0
- magmacrunch_backgammon-0.1.0/pyproject.toml +48 -0
- magmacrunch_backgammon-0.1.0/tests/conftest.py +51 -0
- magmacrunch_backgammon-0.1.0/tests/test_app.py +313 -0
- magmacrunch_backgammon-0.1.0/tests/test_online.py +433 -0
- magmacrunch_backgammon-0.1.0/tests/test_oracle.py +103 -0
- magmacrunch_backgammon-0.1.0/tests/test_rules.py +124 -0
- magmacrunch_backgammon-0.1.0/tests/test_server_parity.py +76 -0
- magmacrunch_backgammon-0.1.0/tools/js_oracle.mjs +130 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Required Notice: Copyright 2026 magmacrunch media (https://magmacrunch.com)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
backgammon
|
|
2
|
+
Copyright 2026 magmacrunch media
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
THE LICENCE
|
|
6
|
+
|
|
7
|
+
Everything in this repository -- the game in web/, its terminal version in
|
|
8
|
+
tui/, its tests, the tools and the documentation -- is licensed under the
|
|
9
|
+
PolyForm Noncommercial License 1.0.0, in the LICENSE file beside this one.
|
|
10
|
+
|
|
11
|
+
Read it, learn from it, build on it, play with it. Any noncommercial purpose is
|
|
12
|
+
permitted, and that includes personal projects, teaching and research. What is not
|
|
13
|
+
permitted is commercial use: selling the game or a derivative of it, shipping it inside
|
|
14
|
+
a product, or running it as part of a business.
|
|
15
|
+
|
|
16
|
+
Backgammon is a traditional game and nobody's property; this licence does not pretend
|
|
17
|
+
otherwise. What is licensed is this implementation: the rules engine, the board
|
|
18
|
+
renderer, the server and the arcade's presentation of them.
|
|
19
|
+
|
|
20
|
+
What is reserved: the magmacrunch name and marks and the visual design of the arcade.
|
|
21
|
+
If you want to use any of it, ask.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
THIRD-PARTY COMPONENTS
|
|
25
|
+
|
|
26
|
+
Fonts are loaded from Google Fonts under the SIL Open Font License 1.1 rather than
|
|
27
|
+
shipped here as files.
|
|
28
|
+
|
|
29
|
+
The page loads adenosine-multiplayer.js, arcade-base.css, board-game-template.js, mp-server.js from the website's arcade/shared/, and the server imports server_base from arcade/shared/multiplayer/.
|
|
30
|
+
Those are part of the magmacrunch.com repository and adenosine, and are not copied here.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: magmacrunch-backgammon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Backgammon against the computer. Terminal version.
|
|
5
|
+
Project-URL: Homepage, https://magmacrunch.com/arcade/backgammon/
|
|
6
|
+
Author: magmacrunch media
|
|
7
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Keywords: arcade,backgammon,board,games,terminal,tui
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: magmacrunch>=0.7.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
16
|
+
Requires-Dist: websockets>=14.0; extra == 'dev'
|
|
17
|
+
Provides-Extra: online
|
|
18
|
+
Requires-Dist: websockets>=14.0; extra == 'online'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Backgammon (terminal)
|
|
22
|
+
|
|
23
|
+
Backgammon against the computer, in a terminal. It is the second version of
|
|
24
|
+
the game, beside `web/`. It runs on the
|
|
25
|
+
[magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
|
|
26
|
+
terminal backend, either as a cabinet in the magmacrunch arcade or as its own
|
|
27
|
+
command, and plays the same either way.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
pipx install magmacrunch-backgammon
|
|
31
|
+
backgammon
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For working on it, from this folder:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
python -m backgammon
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Keys
|
|
42
|
+
|
|
43
|
+
| key | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `←` `↑` `↓` `→` (or `h` `j` `k` `l`) | move the cursor along the points, and between the top and bottom rows |
|
|
46
|
+
| `ENTER` | pick up the checker under the cursor; on a marked point, play that step |
|
|
47
|
+
| `Esc` | put the checker back; then take back the last step; then go to the title (and from there, the arcade) |
|
|
48
|
+
| `D` | double, before you move |
|
|
49
|
+
| `R` | new game |
|
|
50
|
+
| `Q` | quit |
|
|
51
|
+
|
|
52
|
+
You play cyan, `███`, moving down the board from 24 to 1 and bearing off at the
|
|
53
|
+
bottom right. The computer plays pink, `▒▒▒`. The two differ by shape as well
|
|
54
|
+
as colour, so they stay apart under `NO_COLOR` and `--ascii` (`###` and `+++`).
|
|
55
|
+
The points you can move from have green numbers. Once a checker is picked up,
|
|
56
|
+
a `✦` marks where each die can take it: green for a plain step, yellow for a
|
|
57
|
+
hit. The cursor is an arrow in the middle of the board as well as a shade. The
|
|
58
|
+
panel shows the dice still to play, the cube, both pip counts and the last few
|
|
59
|
+
moves. The computer answers after a second, as it does on the page.
|
|
60
|
+
|
|
61
|
+
## Moving one step at a time
|
|
62
|
+
|
|
63
|
+
Each die is played as its own step. When you put a checker down, the screen
|
|
64
|
+
asks for the next die. The move is sent once no further step can follow, so you
|
|
65
|
+
play as much of the roll as the rules allow. `Esc` takes a step back.
|
|
66
|
+
|
|
67
|
+
This is how the page plays online. The page's single-player game does
|
|
68
|
+
something else: a click plays the *first whole move* in its list that includes
|
|
69
|
+
that step. With a 6-1, clicking 8 to 7 can also move a different checker from
|
|
70
|
+
8 to 2. The rules are the same; only the way you pick among the moves differs.
|
|
71
|
+
|
|
72
|
+
## Playing a person online (a prototype)
|
|
73
|
+
|
|
74
|
+
PLAY ONLINE on the title, or `backgammon --online`, connects to the arcade's
|
|
75
|
+
own multiplayer server, the one behind the page's PLAY ONLINE button, so a
|
|
76
|
+
terminal and a browser can share a room. Type a name, then press `C` to create
|
|
77
|
+
a room and read its four-letter code to your opponent, or `J` to join one. The
|
|
78
|
+
host starts the game. Each of you sees your own checkers in cyan, moving down
|
|
79
|
+
to the bottom right: the second player's board is turned round, as the page
|
|
80
|
+
does. You move exactly as against the computer, a step at a time. `D` offers a
|
|
81
|
+
double; when one is offered to you, `Y` takes and `N` passes. `X` then `Y`
|
|
82
|
+
resigns.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
pip install "magmacrunch-backgammon[online]" # websockets, only for this
|
|
86
|
+
backgammon --online
|
|
87
|
+
backgammon --online --server ws://127.0.0.1:8771 # a web/server.py on this machine
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The server keeps the rules, and this client needs none of them. Every state
|
|
91
|
+
the server sends lists the mover's legal moves in full, and a move goes back
|
|
92
|
+
as an index into that list. The screen builds your move one step at a time from
|
|
93
|
+
that list, then sends its index. `backgammon/online.py` holds the protocol,
|
|
94
|
+
split into `Table` (the server's state, turned to your seat, with no I/O) and
|
|
95
|
+
`Connection` (the WebSocket, on its own thread). `online_scenes.py` reuses the
|
|
96
|
+
computer game's screen: it gets the server's state in place of a local game.
|
|
97
|
+
|
|
98
|
+
`tests/test_online.py` runs `web/server.py` in-process on a free port and
|
|
99
|
+
plays it over real sockets. It covers:
|
|
100
|
+
|
|
101
|
+
- **a whole game from both seats.** After every move, both boards must be
|
|
102
|
+
mirror images, match a shadow board kept with `rules.py`, and offer exactly
|
|
103
|
+
the moves `rules.py` gives for each player's own view. The game includes
|
|
104
|
+
hits, entries from the bar, bearing off and turns passed;
|
|
105
|
+
- the passes the server reports, the fallback for a server that doesn't, and
|
|
106
|
+
a game that ends without a winner, read without a socket;
|
|
107
|
+
- **the cube** offered, taken, redoubled and passed;
|
|
108
|
+
- out-of-turn and out-of-range moves, which the server ignores;
|
|
109
|
+
- a wrong room code;
|
|
110
|
+
- an opponent leaving;
|
|
111
|
+
- **two complete terminal sessions**, through the lobby, a move from each seat
|
|
112
|
+
and a resignation, using keys alone.
|
|
113
|
+
|
|
114
|
+
It never contacts the live server. By hand, the real page in headless Chromium
|
|
115
|
+
has played a terminal client in the second, mirrored seat through a whole game
|
|
116
|
+
to the last checker borne off. The two boards matched, with no page errors.
|
|
117
|
+
|
|
118
|
+
What a prototype has not done yet:
|
|
119
|
+
|
|
120
|
+
- **Nobody has played the live server from a terminal**, only a copy of it run
|
|
121
|
+
locally. The default address is `wss://magmacrunch.duckdns.org/backgammon`,
|
|
122
|
+
which is what `mpServerFor('backgammon', 8771)` gives the public site.
|
|
123
|
+
- **No reconnect.** A dropped connection says so, and the game is lost.
|
|
124
|
+
- **No chat**, which the browser has.
|
|
125
|
+
- **States can be large.** With doubles early in a game, the list of legal
|
|
126
|
+
moves runs to thousands, and one state measured 1.9 MB. That is past
|
|
127
|
+
websockets' default limit of 1 MiB a frame, so this client raises its limit.
|
|
128
|
+
The browser has no limit. Any other client of this server will need the
|
|
129
|
+
same.
|
|
130
|
+
- **Passes come from the server now, but not yet from the live one.** When
|
|
131
|
+
a side has no move, the server rolls for the other side at once. It now
|
|
132
|
+
lists those passes in the state, and this client logs them with their dice.
|
|
133
|
+
The live server will only do this once this branch is synced. Until then
|
|
134
|
+
the client falls back to noticing that the turn came straight back to the
|
|
135
|
+
side that just moved. The browser doesn't show passes either way.
|
|
136
|
+
|
|
137
|
+
## It is the browser's game, and the browser's opponent
|
|
138
|
+
|
|
139
|
+
`backgammon/rules.py` is `web/js/board.js`, `dice.js`, `ai.js` and `game.js`,
|
|
140
|
+
ported function for function. Three tests hold it to the other versions:
|
|
141
|
+
|
|
142
|
+
- **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
|
|
143
|
+
unmodified, with `Math.random` replaced by a seeded generator. It plays 120
|
|
144
|
+
games through `Game`'s own API: half against a random player, half against
|
|
145
|
+
a careful one who wins about half of its games. The test replays every game
|
|
146
|
+
through the port with the same seed and requires the same state after each
|
|
147
|
+
of about 10,000 moves: whose turn, the dice, every place on the board, every
|
|
148
|
+
legal move in the page's order, and the cube.
|
|
149
|
+
- **`tests/test_server_parity.py`.** It compares the port's move generator
|
|
150
|
+
with `web/server.py`'s for every roll and both sides, in positions from
|
|
151
|
+
played games, so single-player and online offer the same moves.
|
|
152
|
+
- **`tests/test_rules.py`.** It sets up by hand the cases the oracle only
|
|
153
|
+
reaches by chance.
|
|
154
|
+
|
|
155
|
+
## Two bugs fixed in both, and the quirks kept
|
|
156
|
+
|
|
157
|
+
**Entering onto a blot used to remove both checkers.** `board.js` typed every
|
|
158
|
+
entry from the bar as an ordinary entry, even onto the other side's blot. The
|
|
159
|
+
blot was never sent to the bar: the entering checker cancelled it out and
|
|
160
|
+
both left the game. A side short of 15 checkers can never bear off 15, so 188
|
|
161
|
+
of 200 random games could not end. This is fixed in `web/js/board.js` and here
|
|
162
|
+
together. `web/server.py` already got it right, so online play was unaffected.
|
|
163
|
+
|
|
164
|
+
**The computer used to judge its moves on a board with one sign wrong.** It
|
|
165
|
+
scores each step of a move on a scratch board, applying the steps before it.
|
|
166
|
+
`ai.js` put its own checker down there in the other side's colour. So after
|
|
167
|
+
entering onto its blot on 5 to make a point, it saw 5 as empty, and spent the
|
|
168
|
+
next die breaking the point it had just made. One decision in thirteen was
|
|
169
|
+
judged this way. Fixed in `web/js/ai.js` and here together, the computer beats
|
|
170
|
+
its old self in 63% of 2,000 games.
|
|
171
|
+
|
|
172
|
+
These quirks are kept, because the oracle requires them:
|
|
173
|
+
|
|
174
|
+
- **The opening is a roll-off of two dice each**, and the winner moves with
|
|
175
|
+
its own pair. Standard backgammon rolls one die each and moves with both.
|
|
176
|
+
- **The rules never insist on the larger die** when only one die can be
|
|
177
|
+
played. With doubles they offer moves of one to four steps. Here you are
|
|
178
|
+
asked for every step you can play anyway.
|
|
179
|
+
- **The cube can be turned after the roll**, before you move. The computer
|
|
180
|
+
always takes and never doubles. A game is worth the cube, and there are no
|
|
181
|
+
gammons.
|
|
182
|
+
|
|
183
|
+
There is no high-score table, because the page keeps none for backgammon.
|
|
184
|
+
|
|
185
|
+
Needs a 74x17 terminal.
|
|
186
|
+
|
|
187
|
+
## Licence
|
|
188
|
+
|
|
189
|
+
PolyForm Noncommercial 1.0.0, as for the rest of this repository. See `LICENSE`
|
|
190
|
+
and `NOTICE`.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Backgammon (terminal)
|
|
2
|
+
|
|
3
|
+
Backgammon against the computer, in a terminal. It is the second version of
|
|
4
|
+
the game, beside `web/`. It runs on the
|
|
5
|
+
[magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
|
|
6
|
+
terminal backend, either as a cabinet in the magmacrunch arcade or as its own
|
|
7
|
+
command, and plays the same either way.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pipx install magmacrunch-backgammon
|
|
11
|
+
backgammon
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For working on it, from this folder:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
python -m backgammon
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Keys
|
|
22
|
+
|
|
23
|
+
| key | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `←` `↑` `↓` `→` (or `h` `j` `k` `l`) | move the cursor along the points, and between the top and bottom rows |
|
|
26
|
+
| `ENTER` | pick up the checker under the cursor; on a marked point, play that step |
|
|
27
|
+
| `Esc` | put the checker back; then take back the last step; then go to the title (and from there, the arcade) |
|
|
28
|
+
| `D` | double, before you move |
|
|
29
|
+
| `R` | new game |
|
|
30
|
+
| `Q` | quit |
|
|
31
|
+
|
|
32
|
+
You play cyan, `███`, moving down the board from 24 to 1 and bearing off at the
|
|
33
|
+
bottom right. The computer plays pink, `▒▒▒`. The two differ by shape as well
|
|
34
|
+
as colour, so they stay apart under `NO_COLOR` and `--ascii` (`###` and `+++`).
|
|
35
|
+
The points you can move from have green numbers. Once a checker is picked up,
|
|
36
|
+
a `✦` marks where each die can take it: green for a plain step, yellow for a
|
|
37
|
+
hit. The cursor is an arrow in the middle of the board as well as a shade. The
|
|
38
|
+
panel shows the dice still to play, the cube, both pip counts and the last few
|
|
39
|
+
moves. The computer answers after a second, as it does on the page.
|
|
40
|
+
|
|
41
|
+
## Moving one step at a time
|
|
42
|
+
|
|
43
|
+
Each die is played as its own step. When you put a checker down, the screen
|
|
44
|
+
asks for the next die. The move is sent once no further step can follow, so you
|
|
45
|
+
play as much of the roll as the rules allow. `Esc` takes a step back.
|
|
46
|
+
|
|
47
|
+
This is how the page plays online. The page's single-player game does
|
|
48
|
+
something else: a click plays the *first whole move* in its list that includes
|
|
49
|
+
that step. With a 6-1, clicking 8 to 7 can also move a different checker from
|
|
50
|
+
8 to 2. The rules are the same; only the way you pick among the moves differs.
|
|
51
|
+
|
|
52
|
+
## Playing a person online (a prototype)
|
|
53
|
+
|
|
54
|
+
PLAY ONLINE on the title, or `backgammon --online`, connects to the arcade's
|
|
55
|
+
own multiplayer server, the one behind the page's PLAY ONLINE button, so a
|
|
56
|
+
terminal and a browser can share a room. Type a name, then press `C` to create
|
|
57
|
+
a room and read its four-letter code to your opponent, or `J` to join one. The
|
|
58
|
+
host starts the game. Each of you sees your own checkers in cyan, moving down
|
|
59
|
+
to the bottom right: the second player's board is turned round, as the page
|
|
60
|
+
does. You move exactly as against the computer, a step at a time. `D` offers a
|
|
61
|
+
double; when one is offered to you, `Y` takes and `N` passes. `X` then `Y`
|
|
62
|
+
resigns.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
pip install "magmacrunch-backgammon[online]" # websockets, only for this
|
|
66
|
+
backgammon --online
|
|
67
|
+
backgammon --online --server ws://127.0.0.1:8771 # a web/server.py on this machine
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The server keeps the rules, and this client needs none of them. Every state
|
|
71
|
+
the server sends lists the mover's legal moves in full, and a move goes back
|
|
72
|
+
as an index into that list. The screen builds your move one step at a time from
|
|
73
|
+
that list, then sends its index. `backgammon/online.py` holds the protocol,
|
|
74
|
+
split into `Table` (the server's state, turned to your seat, with no I/O) and
|
|
75
|
+
`Connection` (the WebSocket, on its own thread). `online_scenes.py` reuses the
|
|
76
|
+
computer game's screen: it gets the server's state in place of a local game.
|
|
77
|
+
|
|
78
|
+
`tests/test_online.py` runs `web/server.py` in-process on a free port and
|
|
79
|
+
plays it over real sockets. It covers:
|
|
80
|
+
|
|
81
|
+
- **a whole game from both seats.** After every move, both boards must be
|
|
82
|
+
mirror images, match a shadow board kept with `rules.py`, and offer exactly
|
|
83
|
+
the moves `rules.py` gives for each player's own view. The game includes
|
|
84
|
+
hits, entries from the bar, bearing off and turns passed;
|
|
85
|
+
- the passes the server reports, the fallback for a server that doesn't, and
|
|
86
|
+
a game that ends without a winner, read without a socket;
|
|
87
|
+
- **the cube** offered, taken, redoubled and passed;
|
|
88
|
+
- out-of-turn and out-of-range moves, which the server ignores;
|
|
89
|
+
- a wrong room code;
|
|
90
|
+
- an opponent leaving;
|
|
91
|
+
- **two complete terminal sessions**, through the lobby, a move from each seat
|
|
92
|
+
and a resignation, using keys alone.
|
|
93
|
+
|
|
94
|
+
It never contacts the live server. By hand, the real page in headless Chromium
|
|
95
|
+
has played a terminal client in the second, mirrored seat through a whole game
|
|
96
|
+
to the last checker borne off. The two boards matched, with no page errors.
|
|
97
|
+
|
|
98
|
+
What a prototype has not done yet:
|
|
99
|
+
|
|
100
|
+
- **Nobody has played the live server from a terminal**, only a copy of it run
|
|
101
|
+
locally. The default address is `wss://magmacrunch.duckdns.org/backgammon`,
|
|
102
|
+
which is what `mpServerFor('backgammon', 8771)` gives the public site.
|
|
103
|
+
- **No reconnect.** A dropped connection says so, and the game is lost.
|
|
104
|
+
- **No chat**, which the browser has.
|
|
105
|
+
- **States can be large.** With doubles early in a game, the list of legal
|
|
106
|
+
moves runs to thousands, and one state measured 1.9 MB. That is past
|
|
107
|
+
websockets' default limit of 1 MiB a frame, so this client raises its limit.
|
|
108
|
+
The browser has no limit. Any other client of this server will need the
|
|
109
|
+
same.
|
|
110
|
+
- **Passes come from the server now, but not yet from the live one.** When
|
|
111
|
+
a side has no move, the server rolls for the other side at once. It now
|
|
112
|
+
lists those passes in the state, and this client logs them with their dice.
|
|
113
|
+
The live server will only do this once this branch is synced. Until then
|
|
114
|
+
the client falls back to noticing that the turn came straight back to the
|
|
115
|
+
side that just moved. The browser doesn't show passes either way.
|
|
116
|
+
|
|
117
|
+
## It is the browser's game, and the browser's opponent
|
|
118
|
+
|
|
119
|
+
`backgammon/rules.py` is `web/js/board.js`, `dice.js`, `ai.js` and `game.js`,
|
|
120
|
+
ported function for function. Three tests hold it to the other versions:
|
|
121
|
+
|
|
122
|
+
- **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
|
|
123
|
+
unmodified, with `Math.random` replaced by a seeded generator. It plays 120
|
|
124
|
+
games through `Game`'s own API: half against a random player, half against
|
|
125
|
+
a careful one who wins about half of its games. The test replays every game
|
|
126
|
+
through the port with the same seed and requires the same state after each
|
|
127
|
+
of about 10,000 moves: whose turn, the dice, every place on the board, every
|
|
128
|
+
legal move in the page's order, and the cube.
|
|
129
|
+
- **`tests/test_server_parity.py`.** It compares the port's move generator
|
|
130
|
+
with `web/server.py`'s for every roll and both sides, in positions from
|
|
131
|
+
played games, so single-player and online offer the same moves.
|
|
132
|
+
- **`tests/test_rules.py`.** It sets up by hand the cases the oracle only
|
|
133
|
+
reaches by chance.
|
|
134
|
+
|
|
135
|
+
## Two bugs fixed in both, and the quirks kept
|
|
136
|
+
|
|
137
|
+
**Entering onto a blot used to remove both checkers.** `board.js` typed every
|
|
138
|
+
entry from the bar as an ordinary entry, even onto the other side's blot. The
|
|
139
|
+
blot was never sent to the bar: the entering checker cancelled it out and
|
|
140
|
+
both left the game. A side short of 15 checkers can never bear off 15, so 188
|
|
141
|
+
of 200 random games could not end. This is fixed in `web/js/board.js` and here
|
|
142
|
+
together. `web/server.py` already got it right, so online play was unaffected.
|
|
143
|
+
|
|
144
|
+
**The computer used to judge its moves on a board with one sign wrong.** It
|
|
145
|
+
scores each step of a move on a scratch board, applying the steps before it.
|
|
146
|
+
`ai.js` put its own checker down there in the other side's colour. So after
|
|
147
|
+
entering onto its blot on 5 to make a point, it saw 5 as empty, and spent the
|
|
148
|
+
next die breaking the point it had just made. One decision in thirteen was
|
|
149
|
+
judged this way. Fixed in `web/js/ai.js` and here together, the computer beats
|
|
150
|
+
its old self in 63% of 2,000 games.
|
|
151
|
+
|
|
152
|
+
These quirks are kept, because the oracle requires them:
|
|
153
|
+
|
|
154
|
+
- **The opening is a roll-off of two dice each**, and the winner moves with
|
|
155
|
+
its own pair. Standard backgammon rolls one die each and moves with both.
|
|
156
|
+
- **The rules never insist on the larger die** when only one die can be
|
|
157
|
+
played. With doubles they offer moves of one to four steps. Here you are
|
|
158
|
+
asked for every step you can play anyway.
|
|
159
|
+
- **The cube can be turned after the roll**, before you move. The computer
|
|
160
|
+
always takes and never doubles. A game is worth the cube, and there are no
|
|
161
|
+
gammons.
|
|
162
|
+
|
|
163
|
+
There is no high-score table, because the page keeps none for backgammon.
|
|
164
|
+
|
|
165
|
+
Needs a 74x17 terminal.
|
|
166
|
+
|
|
167
|
+
## Licence
|
|
168
|
+
|
|
169
|
+
PolyForm Noncommercial 1.0.0, as for the rest of this repository. See `LICENSE`
|
|
170
|
+
and `NOTICE`.
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""``python -m backgammon`` -- play backgammon against the computer in a terminal."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="backgammon", description="Backgammon against the computer.")
|
|
11
|
+
parser.add_argument("--play", action="store_true", help="skip the title screen")
|
|
12
|
+
parser.add_argument("--online", action="store_true",
|
|
13
|
+
help="go straight to PLAY ONLINE (needs the [online] extra)")
|
|
14
|
+
parser.add_argument("--server", metavar="URL",
|
|
15
|
+
help="the multiplayer server for PLAY ONLINE, e.g. "
|
|
16
|
+
"ws://127.0.0.1:8771 for a web/server.py on this machine")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--ascii", action="store_true", dest="ascii_only",
|
|
19
|
+
help="draw with plain ASCII instead of block and box glyphs. Detected "
|
|
20
|
+
"automatically from the terminal's encoding; this forces it. "
|
|
21
|
+
"MAGMACRUNCH_ASCII=1 says the same for every cabinet.",
|
|
22
|
+
)
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
from backgammon.app import run
|
|
26
|
+
|
|
27
|
+
run(args.play, args.ascii_only, server=args.server, online=args.online)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Wiring -- how the screens reach each other.
|
|
2
|
+
|
|
3
|
+
Everything that draws lives in :mod:`backgammon.scenes`; everything that
|
|
4
|
+
decides lives in :mod:`backgammon.rules`. **The terminal is not owned here**:
|
|
5
|
+
it belongs to a ``TuiHost``, which is what lets this run as its own command or
|
|
6
|
+
be seated by the arcade. There is no score table: the page keeps none for
|
|
7
|
+
backgammon.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import random
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from backgammon.arcade import INFO
|
|
17
|
+
from backgammon.scenes import GameScene, TitleScene
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BackgammonApp:
|
|
21
|
+
def __init__(self, host: Any, rand: Callable[[], float] | None = None,
|
|
22
|
+
server: str | None = None):
|
|
23
|
+
self.host = host
|
|
24
|
+
#: Where PLAY ONLINE connects; None for the arcade's public server.
|
|
25
|
+
self.server = server
|
|
26
|
+
#: Stands in for the page's Math.random. Tests pass a seeded one.
|
|
27
|
+
self.rand = rand or random.random
|
|
28
|
+
self.root_scene = TitleScene(self)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def renderer(self):
|
|
32
|
+
return self.host.renderer
|
|
33
|
+
|
|
34
|
+
def start_game(self) -> None:
|
|
35
|
+
self.host.push_scene(GameScene(self))
|
|
36
|
+
|
|
37
|
+
def play_online(self) -> None:
|
|
38
|
+
from backgammon.online import DEFAULT_SERVER
|
|
39
|
+
from backgammon.online_scenes import OnlineLobbyScene
|
|
40
|
+
|
|
41
|
+
self.host.push_scene(OnlineLobbyScene(self, self.server or DEFAULT_SERVER))
|
|
42
|
+
|
|
43
|
+
def show_rules(self) -> None:
|
|
44
|
+
from backgammon.scenes import RulesScene
|
|
45
|
+
|
|
46
|
+
self.host.push_scene(RulesScene(self))
|
|
47
|
+
|
|
48
|
+
def to_title(self) -> None:
|
|
49
|
+
self.host.pop_scene()
|
|
50
|
+
|
|
51
|
+
def leave(self) -> None:
|
|
52
|
+
self.host.pop_scene()
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def scene(self):
|
|
56
|
+
return self.host.scene
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(skip_title: bool = False, ascii_only: bool = False, server: str | None = None,
|
|
60
|
+
online: bool = False) -> None:
|
|
61
|
+
"""Play backgammon as its own command."""
|
|
62
|
+
from magmacrunch.engine.core.tui_host import TuiHost
|
|
63
|
+
from magmacrunch.engine.ui.glyphs import Glyphs
|
|
64
|
+
|
|
65
|
+
host = TuiHost(title=INFO.title, fps=INFO.fps, hold_ms=INFO.hold_ms,
|
|
66
|
+
glyphs=Glyphs.detect(ascii_only=ascii_only))
|
|
67
|
+
app = BackgammonApp(host, server=server)
|
|
68
|
+
host.push_scene(app.root_scene)
|
|
69
|
+
if online:
|
|
70
|
+
app.play_online()
|
|
71
|
+
elif skip_title:
|
|
72
|
+
host.push_scene(GameScene(app))
|
|
73
|
+
host.run()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""What the arcade needs to know to launch this game. Kept tiny: a menu loads
|
|
2
|
+
one of these per installed game just to draw a card."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from magmacrunch.engine.arcade import GameInfo
|
|
9
|
+
|
|
10
|
+
from backgammon import theme
|
|
11
|
+
|
|
12
|
+
INFO = GameInfo(
|
|
13
|
+
key="backgammon",
|
|
14
|
+
title="Backgammon",
|
|
15
|
+
blurb="Against the computer, with the doubling cube.",
|
|
16
|
+
# Turn-based: the frame rate paces the computer's one-second pause and
|
|
17
|
+
# the cursor, nothing else. Edge input, so one arrow is one point.
|
|
18
|
+
fps=20,
|
|
19
|
+
hold_ms=0,
|
|
20
|
+
min_cols=theme.MIN_COLS,
|
|
21
|
+
min_rows=theme.MIN_ROWS,
|
|
22
|
+
accent=theme.PLAYER,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BackgammonGame:
|
|
27
|
+
"""Satisfies :class:`magmacrunch.engine.arcade.ArcadeGame`."""
|
|
28
|
+
|
|
29
|
+
info = INFO
|
|
30
|
+
|
|
31
|
+
def start(self, host: Any) -> Any:
|
|
32
|
+
from backgammon.app import BackgammonApp
|
|
33
|
+
|
|
34
|
+
return BackgammonApp(host).root_scene
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
GAME = BackgammonGame()
|
|
38
|
+
|
|
39
|
+
__all__ = ["GAME", "INFO", "BackgammonGame"]
|