magmacrunch-cribbage 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_cribbage-0.1.0/.gitignore +9 -0
- magmacrunch_cribbage-0.1.0/LICENSE +75 -0
- magmacrunch_cribbage-0.1.0/NOTICE +34 -0
- magmacrunch_cribbage-0.1.0/PKG-INFO +148 -0
- magmacrunch_cribbage-0.1.0/README.md +128 -0
- magmacrunch_cribbage-0.1.0/cribbage/__init__.py +0 -0
- magmacrunch_cribbage-0.1.0/cribbage/__main__.py +34 -0
- magmacrunch_cribbage-0.1.0/cribbage/ai.py +124 -0
- magmacrunch_cribbage-0.1.0/cribbage/app.py +111 -0
- magmacrunch_cribbage-0.1.0/cribbage/arcade.py +39 -0
- magmacrunch_cribbage-0.1.0/cribbage/cards.py +221 -0
- magmacrunch_cribbage-0.1.0/cribbage/game.py +344 -0
- magmacrunch_cribbage-0.1.0/cribbage/online.py +377 -0
- magmacrunch_cribbage-0.1.0/cribbage/online_scenes.py +352 -0
- magmacrunch_cribbage-0.1.0/cribbage/scenes.py +694 -0
- magmacrunch_cribbage-0.1.0/cribbage/theme.py +58 -0
- magmacrunch_cribbage-0.1.0/pyproject.toml +55 -0
- magmacrunch_cribbage-0.1.0/tests/conftest.py +56 -0
- magmacrunch_cribbage-0.1.0/tests/test_app.py +311 -0
- magmacrunch_cribbage-0.1.0/tests/test_cards.py +94 -0
- magmacrunch_cribbage-0.1.0/tests/test_game.py +58 -0
- magmacrunch_cribbage-0.1.0/tests/test_online.py +407 -0
- magmacrunch_cribbage-0.1.0/tests/test_oracle.py +117 -0
- magmacrunch_cribbage-0.1.0/tools/js_oracle.mjs +120 -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,34 @@
|
|
|
1
|
+
cribbage
|
|
2
|
+
Copyright 2026 magmacrunch media
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
THE LICENCE
|
|
6
|
+
|
|
7
|
+
Everything in this repository -- the browser client and the WebSocket server
|
|
8
|
+
in web/, the terminal version in tui/, the tools, and the documentation -- is
|
|
9
|
+
licensed under the PolyForm Noncommercial License 1.0.0, in the LICENSE file
|
|
10
|
+
beside this one.
|
|
11
|
+
|
|
12
|
+
Read it, learn from it, build on it, play with it. Any noncommercial purpose is
|
|
13
|
+
permitted, and that includes personal projects, teaching and research. What is
|
|
14
|
+
not permitted is commercial use: selling the game or a derivative of it,
|
|
15
|
+
shipping it inside a product, or running it as part of a business.
|
|
16
|
+
|
|
17
|
+
Cribbage is a traditional card game and nobody's property; this licence
|
|
18
|
+
does not pretend otherwise. What is licensed is this implementation: the rules, the scoring, the AI opponent, the board renderer and the
|
|
19
|
+
server.
|
|
20
|
+
|
|
21
|
+
What is reserved: the magmacrunch name and marks and the visual design of the
|
|
22
|
+
arcade. If you want to use any of it, ask.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
THIRD-PARTY COMPONENTS
|
|
26
|
+
|
|
27
|
+
Fonts are loaded from Google Fonts under the SIL Open Font License 1.1 rather
|
|
28
|
+
than shipped here as files.
|
|
29
|
+
|
|
30
|
+
The client loads the adenosine cards, multiplayer and score bundles, the deck
|
|
31
|
+
art and the arcade styles from the website's arcade/shared/, and the server
|
|
32
|
+
imports server_base and card_constants from arcade/shared/multiplayer/. Those
|
|
33
|
+
are part of the magmacrunch.com repository and adenosine, and are not copied
|
|
34
|
+
here.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: magmacrunch-cribbage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cribbage against the computer. Terminal version.
|
|
5
|
+
Project-URL: Homepage, https://magmacrunch.com/arcade/cribbage/
|
|
6
|
+
Author: magmacrunch media
|
|
7
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Keywords: arcade,cards,cribbage,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
|
+
# Cribbage (terminal)
|
|
22
|
+
|
|
23
|
+
Cribbage against the computer, in a terminal. The second version of the game
|
|
24
|
+
beside `web/`, on the [magmacrunch](https://github.com/magmacrunch-media/magmacrunch)
|
|
25
|
+
engine's terminal backend: a cabinet in the magmacrunch arcade and its own
|
|
26
|
+
command, the same either way.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pipx install magmacrunch-cribbage
|
|
30
|
+
cribbage
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For working on it, from this folder:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
node ../tools/link-shared.mjs # once: the oracle needs ../shared
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
python -m cribbage
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Keys
|
|
42
|
+
|
|
43
|
+
| key | |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `←` `→` | choose a card |
|
|
46
|
+
| `SPACE` or `1`-`6` | mark it for the crib, or lay it when pegging |
|
|
47
|
+
| `ENTER` | send the two to the crib, cut, lay the chosen card, or carry on after the show |
|
|
48
|
+
| `R` | new game |
|
|
49
|
+
| `Esc` | the title (from the title, back to the arcade) |
|
|
50
|
+
| `Q` | quit |
|
|
51
|
+
|
|
52
|
+
A card that would take the count past 31 is greyed and refused. The
|
|
53
|
+
computer's moves arrive one at a time, under a second apart, so a pegging
|
|
54
|
+
exchange can be followed. The show waits for Enter, because a count is the
|
|
55
|
+
thing a cribbage player wants to check.
|
|
56
|
+
|
|
57
|
+
## Playing a person online (a prototype)
|
|
58
|
+
|
|
59
|
+
PLAY ONLINE on the title, or `cribbage --online`, connects to the arcade's
|
|
60
|
+
own multiplayer server -- the one the browser's Multiplayer button uses -- so
|
|
61
|
+
a terminal and a browser can share a room. Type a name, then `C` to create a
|
|
62
|
+
room and read its four-letter code to your opponent, or `J` to join one by
|
|
63
|
+
its code. The host starts the game once two are in.
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
pip install "magmacrunch-cribbage[online]" # websockets, only for this
|
|
67
|
+
cribbage --online
|
|
68
|
+
cribbage --online --server ws://127.0.0.1:8766 # a web/server.py on this machine
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The server keeps the rules. This client sends what you chose and draws what
|
|
72
|
+
the server says, so it never disagrees with the browser about a score: the
|
|
73
|
+
two are clients of one referee. `cribbage/online.py` has the protocol, split
|
|
74
|
+
into `Table` (the server's state, no I/O, tested without a socket) and
|
|
75
|
+
`Connection` (the WebSocket, on a thread of its own, polled once a frame).
|
|
76
|
+
|
|
77
|
+
`tests/test_online.py` runs `web/server.py` in-process on a free port and
|
|
78
|
+
plays it over real sockets: two clients through a whole game to 121, and two
|
|
79
|
+
full terminal sessions through the lobby and a hand with keys alone. It
|
|
80
|
+
never contacts the live server.
|
|
81
|
+
|
|
82
|
+
What a prototype has not done yet:
|
|
83
|
+
|
|
84
|
+
- **Nobody has played the live server from a terminal**, only a copy of it
|
|
85
|
+
run locally. The default address is `wss://magmacrunch.duckdns.org/cribbage`,
|
|
86
|
+
which is what `mpServerFor('cribbage', 8766)` gives the public site.
|
|
87
|
+
- **No reconnect.** A dropped connection says so and the game is lost.
|
|
88
|
+
- **No chat and no colour pick**, both of which the browser lobby has.
|
|
89
|
+
- **Online games do not reach the high score table**, here or on the page.
|
|
90
|
+
- **The server sends both hands to both players.** `get_state` puts every
|
|
91
|
+
hand in `playerHands`; the browser and this client both decline to draw the
|
|
92
|
+
opponent's, but anyone reading the socket can see it. That wants fixing in
|
|
93
|
+
`server.py`, for the browser's sake as much as this one's.
|
|
94
|
+
|
|
95
|
+
## It is the browser's game, and the browser's opponent
|
|
96
|
+
|
|
97
|
+
Everything that decides is ported from what the page runs:
|
|
98
|
+
|
|
99
|
+
| here | there |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `cribbage/cards.py` | the deck and `CribbageHandEval` from `adenosine-cards`, in the website's `arcade/shared/` |
|
|
102
|
+
| `cribbage/ai.py` | `web/js/ai.js`, the opponent |
|
|
103
|
+
| `cribbage/game.py` | `web/js/game.js`, and the turn-taking `web/js/main.js` wraps around it |
|
|
104
|
+
|
|
105
|
+
`tests/test_oracle.py` holds the port to the page. `tools/js_oracle.mjs`
|
|
106
|
+
loads `adenosine-cards.js`, `config.js`, `ai.js` and `game.js` unmodified,
|
|
107
|
+
plays 120 games against the page's own AI with a scripted player, and the test
|
|
108
|
+
replays every choice through the port. It requires the same deck, cut, hands,
|
|
109
|
+
crib, count, turn, scores and kept cards after every move: about 15,000 moves,
|
|
110
|
+
both sides winning, some games won on his heels and some on a go. `tests/test_cards.py` carries the 50 scoring cases from
|
|
111
|
+
`web/tests/test-scoring.js`, converted line for line.
|
|
112
|
+
|
|
113
|
+
Wins go to the `cribbage` board the page posts to, as the winner's final score
|
|
114
|
+
with `finalScore` beside it. Losses are not recorded, as on the page.
|
|
115
|
+
|
|
116
|
+
## Who leads when a hand runs out
|
|
117
|
+
|
|
118
|
+
After a series, whoever did not lay the last card leads the next, unless
|
|
119
|
+
their hand is empty: then the other side lays out what it has left, alone,
|
|
120
|
+
and takes the go for the last card. The page used to hand the lead to the
|
|
121
|
+
empty hand, then go straight to the show, so the other hand's cards were
|
|
122
|
+
never pegged. The oracle's first 60 games hit that in 32 of them, dropping 42 cards.
|
|
123
|
+
It was fixed in `web/js/game.js` and here in the same change, and the oracle
|
|
124
|
+
now requires that no game drops a card. The multiplayer server already got
|
|
125
|
+
this right.
|
|
126
|
+
|
|
127
|
+
## Three more, fixed in the page and followed here
|
|
128
|
+
|
|
129
|
+
cribbage#5 fixed three more bugs in `web/js/game.js`, and the port copies all
|
|
130
|
+
three, so the oracle still holds:
|
|
131
|
+
|
|
132
|
+
- **The show counts the kept cards.** Pegging empties the hands, and the show
|
|
133
|
+
used to score those empty hands, so only the crib counted. A game took about
|
|
134
|
+
21 hands; it now takes about ten.
|
|
135
|
+
- **His heels can win.** A jack turned that takes the dealer to 121 ends the
|
|
136
|
+
game at the cut. The page used to play on past 121.
|
|
137
|
+
- **So can the go.** The point for a go or the last card can end the game
|
|
138
|
+
too. The page used to reset the series and peg on.
|
|
139
|
+
|
|
140
|
+
The oracle requires a game a little under 13 hands on average, and its 120
|
|
141
|
+
games include wins on his heels and on a go.
|
|
142
|
+
|
|
143
|
+
Needs a 60x22 terminal.
|
|
144
|
+
|
|
145
|
+
## Licence
|
|
146
|
+
|
|
147
|
+
[PolyForm Noncommercial 1.0.0](LICENSE), as the rest of this repository. See
|
|
148
|
+
[NOTICE](NOTICE).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Cribbage (terminal)
|
|
2
|
+
|
|
3
|
+
Cribbage against the computer, in a terminal. The second version of the game
|
|
4
|
+
beside `web/`, on the [magmacrunch](https://github.com/magmacrunch-media/magmacrunch)
|
|
5
|
+
engine's terminal backend: a cabinet in the magmacrunch arcade and its own
|
|
6
|
+
command, the same either way.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pipx install magmacrunch-cribbage
|
|
10
|
+
cribbage
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For working on it, from this folder:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
node ../tools/link-shared.mjs # once: the oracle needs ../shared
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
python -m cribbage
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Keys
|
|
22
|
+
|
|
23
|
+
| key | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `←` `→` | choose a card |
|
|
26
|
+
| `SPACE` or `1`-`6` | mark it for the crib, or lay it when pegging |
|
|
27
|
+
| `ENTER` | send the two to the crib, cut, lay the chosen card, or carry on after the show |
|
|
28
|
+
| `R` | new game |
|
|
29
|
+
| `Esc` | the title (from the title, back to the arcade) |
|
|
30
|
+
| `Q` | quit |
|
|
31
|
+
|
|
32
|
+
A card that would take the count past 31 is greyed and refused. The
|
|
33
|
+
computer's moves arrive one at a time, under a second apart, so a pegging
|
|
34
|
+
exchange can be followed. The show waits for Enter, because a count is the
|
|
35
|
+
thing a cribbage player wants to check.
|
|
36
|
+
|
|
37
|
+
## Playing a person online (a prototype)
|
|
38
|
+
|
|
39
|
+
PLAY ONLINE on the title, or `cribbage --online`, connects to the arcade's
|
|
40
|
+
own multiplayer server -- the one the browser's Multiplayer button uses -- so
|
|
41
|
+
a terminal and a browser can share a room. Type a name, then `C` to create a
|
|
42
|
+
room and read its four-letter code to your opponent, or `J` to join one by
|
|
43
|
+
its code. The host starts the game once two are in.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
pip install "magmacrunch-cribbage[online]" # websockets, only for this
|
|
47
|
+
cribbage --online
|
|
48
|
+
cribbage --online --server ws://127.0.0.1:8766 # a web/server.py on this machine
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The server keeps the rules. This client sends what you chose and draws what
|
|
52
|
+
the server says, so it never disagrees with the browser about a score: the
|
|
53
|
+
two are clients of one referee. `cribbage/online.py` has the protocol, split
|
|
54
|
+
into `Table` (the server's state, no I/O, tested without a socket) and
|
|
55
|
+
`Connection` (the WebSocket, on a thread of its own, polled once a frame).
|
|
56
|
+
|
|
57
|
+
`tests/test_online.py` runs `web/server.py` in-process on a free port and
|
|
58
|
+
plays it over real sockets: two clients through a whole game to 121, and two
|
|
59
|
+
full terminal sessions through the lobby and a hand with keys alone. It
|
|
60
|
+
never contacts the live server.
|
|
61
|
+
|
|
62
|
+
What a prototype has not done yet:
|
|
63
|
+
|
|
64
|
+
- **Nobody has played the live server from a terminal**, only a copy of it
|
|
65
|
+
run locally. The default address is `wss://magmacrunch.duckdns.org/cribbage`,
|
|
66
|
+
which is what `mpServerFor('cribbage', 8766)` gives the public site.
|
|
67
|
+
- **No reconnect.** A dropped connection says so and the game is lost.
|
|
68
|
+
- **No chat and no colour pick**, both of which the browser lobby has.
|
|
69
|
+
- **Online games do not reach the high score table**, here or on the page.
|
|
70
|
+
- **The server sends both hands to both players.** `get_state` puts every
|
|
71
|
+
hand in `playerHands`; the browser and this client both decline to draw the
|
|
72
|
+
opponent's, but anyone reading the socket can see it. That wants fixing in
|
|
73
|
+
`server.py`, for the browser's sake as much as this one's.
|
|
74
|
+
|
|
75
|
+
## It is the browser's game, and the browser's opponent
|
|
76
|
+
|
|
77
|
+
Everything that decides is ported from what the page runs:
|
|
78
|
+
|
|
79
|
+
| here | there |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `cribbage/cards.py` | the deck and `CribbageHandEval` from `adenosine-cards`, in the website's `arcade/shared/` |
|
|
82
|
+
| `cribbage/ai.py` | `web/js/ai.js`, the opponent |
|
|
83
|
+
| `cribbage/game.py` | `web/js/game.js`, and the turn-taking `web/js/main.js` wraps around it |
|
|
84
|
+
|
|
85
|
+
`tests/test_oracle.py` holds the port to the page. `tools/js_oracle.mjs`
|
|
86
|
+
loads `adenosine-cards.js`, `config.js`, `ai.js` and `game.js` unmodified,
|
|
87
|
+
plays 120 games against the page's own AI with a scripted player, and the test
|
|
88
|
+
replays every choice through the port. It requires the same deck, cut, hands,
|
|
89
|
+
crib, count, turn, scores and kept cards after every move: about 15,000 moves,
|
|
90
|
+
both sides winning, some games won on his heels and some on a go. `tests/test_cards.py` carries the 50 scoring cases from
|
|
91
|
+
`web/tests/test-scoring.js`, converted line for line.
|
|
92
|
+
|
|
93
|
+
Wins go to the `cribbage` board the page posts to, as the winner's final score
|
|
94
|
+
with `finalScore` beside it. Losses are not recorded, as on the page.
|
|
95
|
+
|
|
96
|
+
## Who leads when a hand runs out
|
|
97
|
+
|
|
98
|
+
After a series, whoever did not lay the last card leads the next, unless
|
|
99
|
+
their hand is empty: then the other side lays out what it has left, alone,
|
|
100
|
+
and takes the go for the last card. The page used to hand the lead to the
|
|
101
|
+
empty hand, then go straight to the show, so the other hand's cards were
|
|
102
|
+
never pegged. The oracle's first 60 games hit that in 32 of them, dropping 42 cards.
|
|
103
|
+
It was fixed in `web/js/game.js` and here in the same change, and the oracle
|
|
104
|
+
now requires that no game drops a card. The multiplayer server already got
|
|
105
|
+
this right.
|
|
106
|
+
|
|
107
|
+
## Three more, fixed in the page and followed here
|
|
108
|
+
|
|
109
|
+
cribbage#5 fixed three more bugs in `web/js/game.js`, and the port copies all
|
|
110
|
+
three, so the oracle still holds:
|
|
111
|
+
|
|
112
|
+
- **The show counts the kept cards.** Pegging empties the hands, and the show
|
|
113
|
+
used to score those empty hands, so only the crib counted. A game took about
|
|
114
|
+
21 hands; it now takes about ten.
|
|
115
|
+
- **His heels can win.** A jack turned that takes the dealer to 121 ends the
|
|
116
|
+
game at the cut. The page used to play on past 121.
|
|
117
|
+
- **So can the go.** The point for a go or the last card can end the game
|
|
118
|
+
too. The page used to reset the series and peg on.
|
|
119
|
+
|
|
120
|
+
The oracle requires a game a little under 13 hands on average, and its 120
|
|
121
|
+
games include wins on his heels and on a go.
|
|
122
|
+
|
|
123
|
+
Needs a 60x22 terminal.
|
|
124
|
+
|
|
125
|
+
## Licence
|
|
126
|
+
|
|
127
|
+
[PolyForm Noncommercial 1.0.0](LICENSE), as the rest of this repository. See
|
|
128
|
+
[NOTICE](NOTICE).
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""``python -m cribbage`` -- play cribbage 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="cribbage", description="Cribbage against the computer.")
|
|
11
|
+
parser.add_argument("--seed", type=int, default=None,
|
|
12
|
+
help="fix the session's deals, for a reproducible one")
|
|
13
|
+
parser.add_argument("--play", action="store_true", help="skip the title screen")
|
|
14
|
+
parser.add_argument("--online", action="store_true",
|
|
15
|
+
help="go straight to the online lobby (a prototype)")
|
|
16
|
+
parser.add_argument("--server", default=None,
|
|
17
|
+
help="the multiplayer server to play online through, "
|
|
18
|
+
"e.g. ws://127.0.0.1:8766 for a web/server.py run "
|
|
19
|
+
"locally. Default: the arcade's own")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--ascii", action="store_true", dest="ascii_only",
|
|
22
|
+
help="draw with plain ASCII instead of box, suit and block glyphs. "
|
|
23
|
+
"Detected automatically from the terminal's encoding; this "
|
|
24
|
+
"forces it. MAGMACRUNCH_ASCII=1 says the same for every cabinet.",
|
|
25
|
+
)
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
from cribbage.app import run
|
|
29
|
+
|
|
30
|
+
run(args.seed, args.play, args.ascii_only, args.server, args.online)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""The opponent: ``CribbageAI`` from ``web/js/ai.js``, line for line.
|
|
2
|
+
|
|
3
|
+
Two decisions and nothing else -- which two cards to throw to the crib, and
|
|
4
|
+
which card to lay while pegging -- each made by scoring every option with a
|
|
5
|
+
hand-tuned heuristic and taking the first best. Ties go to the earliest
|
|
6
|
+
option, as ``score > bestScore`` gives them in the browser, which is why the
|
|
7
|
+
loops below walk the hand in the same order it does.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from cribbage.cards import Card, order, value
|
|
15
|
+
|
|
16
|
+
MAX_PEG_COUNT = 31
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def select_crib_cards(hand: Sequence[Card], crib_is_opponents: bool) -> list[Card]:
|
|
20
|
+
"""Two cards to discard. ``crib_is_opponents`` is True when the human
|
|
21
|
+
deals, so the crib these land in is not the AI's to score."""
|
|
22
|
+
if len(hand) != 6:
|
|
23
|
+
return list(hand[:2])
|
|
24
|
+
best: list[Card] | None = None
|
|
25
|
+
best_score = float("-inf")
|
|
26
|
+
for i in range(len(hand)):
|
|
27
|
+
for j in range(i + 1, len(hand)):
|
|
28
|
+
discard = [hand[i], hand[j]]
|
|
29
|
+
remaining = [c for k, c in enumerate(hand) if k not in (i, j)]
|
|
30
|
+
score = score_crib_discard(remaining, discard, crib_is_opponents)
|
|
31
|
+
if score > best_score:
|
|
32
|
+
best_score = score
|
|
33
|
+
best = discard
|
|
34
|
+
return best or list(hand[:2])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def score_crib_discard(remaining: Sequence[Card], discard: Sequence[Card],
|
|
38
|
+
crib_is_opponents: bool) -> float:
|
|
39
|
+
score = 0
|
|
40
|
+
for i in range(len(remaining)):
|
|
41
|
+
for j in range(i + 1, len(remaining)):
|
|
42
|
+
if value(remaining[i].rank) + value(remaining[j].rank) == 15:
|
|
43
|
+
score += 4
|
|
44
|
+
|
|
45
|
+
ranks: dict[str, int] = {}
|
|
46
|
+
for c in remaining:
|
|
47
|
+
ranks[c.rank] = ranks.get(c.rank, 0) + 1
|
|
48
|
+
score += 2 * sum(1 for n in ranks.values() if n == 2)
|
|
49
|
+
|
|
50
|
+
orders = sorted(order(c.rank) for c in remaining)
|
|
51
|
+
for i in range(len(orders) - 2):
|
|
52
|
+
if orders[i + 1] == orders[i] + 1 and orders[i + 2] == orders[i] + 2:
|
|
53
|
+
score += 3
|
|
54
|
+
|
|
55
|
+
suits: dict[str, int] = {}
|
|
56
|
+
for c in remaining:
|
|
57
|
+
suits[c.suit] = suits.get(c.suit, 0) + 1
|
|
58
|
+
score += 2 * sum(1 for n in suits.values() if n == 3)
|
|
59
|
+
|
|
60
|
+
score += sum(1 for c in remaining if c.rank == "5")
|
|
61
|
+
|
|
62
|
+
if crib_is_opponents:
|
|
63
|
+
for c in discard:
|
|
64
|
+
if c.rank == "5":
|
|
65
|
+
score -= 2
|
|
66
|
+
if c.rank in ("10", "J", "Q", "K"):
|
|
67
|
+
score -= 1
|
|
68
|
+
return score
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def select_pegging_card(hand: Sequence[Card], count: int,
|
|
72
|
+
played: Sequence[Card]) -> Card | None:
|
|
73
|
+
"""The card to lay, or None for a go."""
|
|
74
|
+
playable = [c for c in hand if value(c.rank) + count <= MAX_PEG_COUNT]
|
|
75
|
+
if not playable:
|
|
76
|
+
return None
|
|
77
|
+
if len(playable) == 1:
|
|
78
|
+
return playable[0]
|
|
79
|
+
best: Card | None = None
|
|
80
|
+
best_score = float("-inf")
|
|
81
|
+
for c in playable:
|
|
82
|
+
score = score_pegging_play(c, count, played, hand)
|
|
83
|
+
if score > best_score:
|
|
84
|
+
best_score = score
|
|
85
|
+
best = c
|
|
86
|
+
return best or playable[0]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def score_pegging_play(card: Card, count: int, played: Sequence[Card],
|
|
90
|
+
hand: Sequence[Card]) -> float:
|
|
91
|
+
score: float = 0
|
|
92
|
+
new_count = count + value(card.rank)
|
|
93
|
+
|
|
94
|
+
if new_count in (15, 31):
|
|
95
|
+
score += 2
|
|
96
|
+
|
|
97
|
+
if played:
|
|
98
|
+
if card.rank == played[-1].rank:
|
|
99
|
+
score += 2
|
|
100
|
+
if len(played) >= 2 and played[-2].rank == card.rank:
|
|
101
|
+
score += 4
|
|
102
|
+
if len(played) >= 3 and played[-3].rank == card.rank:
|
|
103
|
+
score += 6
|
|
104
|
+
|
|
105
|
+
# Only the last three cards are looked at for a run, as in the browser.
|
|
106
|
+
if len(played) >= 2:
|
|
107
|
+
last = [*played[-2:], card]
|
|
108
|
+
ranked = sorted(last, key=lambda c: order(c.rank))
|
|
109
|
+
if all(i == 0 or order(c.rank) == order(ranked[i - 1].rank) + 1
|
|
110
|
+
for i, c in enumerate(ranked)):
|
|
111
|
+
score += len(last)
|
|
112
|
+
|
|
113
|
+
if new_count not in (15, 31) and new_count in (10, 20, 25):
|
|
114
|
+
score -= 1
|
|
115
|
+
|
|
116
|
+
if new_count > 25:
|
|
117
|
+
score -= value(card.rank) / 10
|
|
118
|
+
|
|
119
|
+
after = [c for c in hand if c != card]
|
|
120
|
+
score += sum(1 for c in after if value(c.rank) + new_count <= MAX_PEG_COUNT) * 0.5
|
|
121
|
+
return score
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["select_crib_cards", "select_pegging_card"]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Wiring -- what outlives one game, and how the screens reach it.
|
|
2
|
+
|
|
3
|
+
Everything that draws lives in :mod:`cribbage.scenes`; everything that decides
|
|
4
|
+
lives in :mod:`cribbage.game`. **The terminal is not owned here**: it belongs
|
|
5
|
+
to a ``TuiHost``, which is what lets this run as its own command or be seated
|
|
6
|
+
by the arcade.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from magmacrunch.engine.scores import ScoreBook
|
|
15
|
+
|
|
16
|
+
from cribbage.arcade import INFO
|
|
17
|
+
from cribbage.scenes import GameScene, TitleScene
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CribbageApp:
|
|
21
|
+
def __init__(self, host: Any, seed: int | None = None,
|
|
22
|
+
scores: ScoreBook | None = None, server: str | None = None):
|
|
23
|
+
self.host = host
|
|
24
|
+
#: The multiplayer server PLAY ONLINE connects to. The arcade's own by
|
|
25
|
+
#: default; ``--server`` points it somewhere else, such as a server.py
|
|
26
|
+
#: running on this machine.
|
|
27
|
+
from cribbage.online import DEFAULT_SERVER
|
|
28
|
+
|
|
29
|
+
self.server = server or DEFAULT_SERVER
|
|
30
|
+
#: Where each game's shuffles and cuts come from. Seeded, so
|
|
31
|
+
#: ``--seed`` replays a whole session.
|
|
32
|
+
self.rng = random.Random(seed)
|
|
33
|
+
#: The board the browser posts to. It records wins only, as the
|
|
34
|
+
#: player's final score, which is what the page saves.
|
|
35
|
+
self.scores = scores or ScoreBook(INFO.scoreboard)
|
|
36
|
+
self.initials = "AAA"
|
|
37
|
+
self.last_rank: int | None = None
|
|
38
|
+
self.root_scene = TitleScene(self)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def renderer(self):
|
|
42
|
+
return self.host.renderer
|
|
43
|
+
|
|
44
|
+
def new_rand(self):
|
|
45
|
+
return random.Random(self.rng.randrange(1 << 32)).random
|
|
46
|
+
|
|
47
|
+
def start_game(self) -> None:
|
|
48
|
+
self.host.push_scene(GameScene(self))
|
|
49
|
+
|
|
50
|
+
def play_online(self) -> None:
|
|
51
|
+
"""The lobby. A prototype: see :mod:`cribbage.online`."""
|
|
52
|
+
from cribbage.online_scenes import OnlineLobbyScene
|
|
53
|
+
|
|
54
|
+
self.host.push_scene(OnlineLobbyScene(self, self.server))
|
|
55
|
+
|
|
56
|
+
def show_scores(self) -> None:
|
|
57
|
+
from cribbage.scenes import ScoresScene
|
|
58
|
+
|
|
59
|
+
self.host.push_scene(ScoresScene(self))
|
|
60
|
+
|
|
61
|
+
def show_rules(self) -> None:
|
|
62
|
+
from cribbage.scenes import RulesScene
|
|
63
|
+
|
|
64
|
+
self.host.push_scene(RulesScene(self))
|
|
65
|
+
|
|
66
|
+
def enter_initials(self, score: int, extra: dict) -> None:
|
|
67
|
+
from cribbage.scenes import InitialsScene
|
|
68
|
+
|
|
69
|
+
self.host.push_scene(InitialsScene(self, score, extra))
|
|
70
|
+
|
|
71
|
+
def to_title(self) -> None:
|
|
72
|
+
self.host.pop_scene()
|
|
73
|
+
|
|
74
|
+
def leave(self) -> None:
|
|
75
|
+
self.host.pop_scene()
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def best(self) -> int:
|
|
79
|
+
return self.scores.best()
|
|
80
|
+
|
|
81
|
+
def qualifies(self, score: int) -> bool:
|
|
82
|
+
return score > 0 and self.scores.qualifies(score)
|
|
83
|
+
|
|
84
|
+
def record(self, score: int, initials: str | None = None,
|
|
85
|
+
extra: dict | None = None):
|
|
86
|
+
self.initials = initials or self.initials
|
|
87
|
+
result = self.scores.save(self.initials, score, **(extra or {}))
|
|
88
|
+
self.last_rank = result.rank
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def scene(self):
|
|
93
|
+
return self.host.scene
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run(seed: int | None = None, skip_title: bool = False,
|
|
97
|
+
ascii_only: bool = False, server: str | None = None,
|
|
98
|
+
online: bool = False) -> None:
|
|
99
|
+
"""Play cribbage as its own command."""
|
|
100
|
+
from magmacrunch.engine.core.tui_host import TuiHost
|
|
101
|
+
from magmacrunch.engine.ui.glyphs import Glyphs
|
|
102
|
+
|
|
103
|
+
host = TuiHost(title=INFO.title, fps=INFO.fps, hold_ms=INFO.hold_ms,
|
|
104
|
+
glyphs=Glyphs.detect(ascii_only=ascii_only))
|
|
105
|
+
app = CribbageApp(host, seed, server=server)
|
|
106
|
+
host.push_scene(app.root_scene)
|
|
107
|
+
if online:
|
|
108
|
+
app.play_online()
|
|
109
|
+
elif skip_title:
|
|
110
|
+
host.push_scene(GameScene(app))
|
|
111
|
+
host.run()
|