magmacrunch-solitaire 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_solitaire-0.1.0/.gitignore +14 -0
- magmacrunch_solitaire-0.1.0/LICENSE +75 -0
- magmacrunch_solitaire-0.1.0/NOTICE +30 -0
- magmacrunch_solitaire-0.1.0/PKG-INFO +127 -0
- magmacrunch_solitaire-0.1.0/README.md +110 -0
- magmacrunch_solitaire-0.1.0/klondike/__init__.py +0 -0
- magmacrunch_solitaire-0.1.0/klondike/__main__.py +27 -0
- magmacrunch_solitaire-0.1.0/klondike/app.py +98 -0
- magmacrunch_solitaire-0.1.0/klondike/arcade.py +39 -0
- magmacrunch_solitaire-0.1.0/klondike/rules.py +404 -0
- magmacrunch_solitaire-0.1.0/klondike/scenes.py +618 -0
- magmacrunch_solitaire-0.1.0/klondike/theme.py +46 -0
- magmacrunch_solitaire-0.1.0/pyproject.toml +50 -0
- magmacrunch_solitaire-0.1.0/tests/conftest.py +53 -0
- magmacrunch_solitaire-0.1.0/tests/test_app.py +320 -0
- magmacrunch_solitaire-0.1.0/tests/test_oracle.py +111 -0
- magmacrunch_solitaire-0.1.0/tests/test_rules.py +263 -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
|
+
solitaire
|
|
2
|
+
Copyright 2026 magmacrunch media
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
THE LICENCE
|
|
6
|
+
|
|
7
|
+
Everything in this repository -- the game in web/, its terminal version in tui/, the
|
|
8
|
+
tools and the documentation -- is licensed under the PolyForm Noncommercial License 1.0.0, in the
|
|
9
|
+
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
|
+
Solitaire 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 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-cards.js, adenosine-score-client.js, arcade-base.css, cards/cards.css, cards/deck-choice.js, score-server.js from the website's arcade/shared/.
|
|
30
|
+
Those are part of the magmacrunch.com repository and adenosine, and are not copied here.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: magmacrunch-solitaire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Klondike solitaire, draw three, as the magmacrunch arcade plays it. Terminal version.
|
|
5
|
+
Project-URL: Homepage, https://magmacrunch.com/arcade/solitaire/
|
|
6
|
+
Author: magmacrunch media
|
|
7
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: NOTICE
|
|
10
|
+
Keywords: arcade,cards,games,klondike,solitaire,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
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Solitaire (terminal)
|
|
19
|
+
|
|
20
|
+
The magmacrunch arcade's Solitaire, Klondike draw three, in a terminal. It is
|
|
21
|
+
the second version of the game, beside `web/`. It runs on the
|
|
22
|
+
[magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
|
|
23
|
+
terminal backend, either as a cabinet in the magmacrunch arcade or as its own
|
|
24
|
+
command, and plays the same either way.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pipx install magmacrunch-solitaire
|
|
28
|
+
solitaire # the title
|
|
29
|
+
solitaire --play --seed 7 # straight to a deal, the same deals every time
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For working on it, from this folder, once the website's `shared/` is linked
|
|
33
|
+
in beside `web/` (`node tools/link-shared.mjs` from the repository's root),
|
|
34
|
+
which the oracle test needs:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install -e ".[dev]"
|
|
38
|
+
python -m klondike
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Keys
|
|
42
|
+
|
|
43
|
+
A cursor stands for the mouse, and ENTER is a click wherever it stands.
|
|
44
|
+
|
|
45
|
+
| key | |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `←` `→` | along the top row (stock, waste, foundations) or across the columns |
|
|
48
|
+
| `↑` `↓` | up and down a column's face-up cards, and between the top row and the columns |
|
|
49
|
+
| `1` to `7` | a column |
|
|
50
|
+
| `ENTER` `SPACE` | click: draw from the stock, pick a card up, or put the held card there |
|
|
51
|
+
| `F` | double-click: a card to its foundation, if it can go |
|
|
52
|
+
| `D` | draw, wherever the cursor is |
|
|
53
|
+
| `X` | click the felt: put the held card down |
|
|
54
|
+
| `N` | deal again |
|
|
55
|
+
| `H` `S` | help, the scores; the clock stops while they are open |
|
|
56
|
+
| `Esc` `Esc` | leave the deal, for the title (and from there, the arcade) |
|
|
57
|
+
| `Q` | quit |
|
|
58
|
+
|
|
59
|
+
The stock shows its back and how many cards are left. The waste shows its
|
|
60
|
+
top three, fanned. A column shows its face-down cards as one back and a
|
|
61
|
+
count, then its face-up cards one to a row. The held card is lit cyan and
|
|
62
|
+
the cursor pink. A win shows the page's win screen: time, moves, score, and
|
|
63
|
+
the time bonus, with a NEW HIGH SCORE badge when the score makes the board.
|
|
64
|
+
It goes on the board with its time beside it, as the page saves it.
|
|
65
|
+
|
|
66
|
+
Needs a 41x24 terminal.
|
|
67
|
+
|
|
68
|
+
## It is the browser's game, on the browser's clock
|
|
69
|
+
|
|
70
|
+
`klondike/rules.py` is `web/js/game.js`, method for method, with the deck and
|
|
71
|
+
its shuffle from the website's `shared/adenosine-cards.js`. The page keeps
|
|
72
|
+
its time with a one-second `setInterval`, and that is part of the rules: the
|
|
73
|
+
time on the table, and the bonus a win is paid, are whatever that timer last
|
|
74
|
+
set. So the port keeps the page's clock too. Its time only moves when the
|
|
75
|
+
screen moves it, by the real time gone, and the interval fires at each of
|
|
76
|
+
its seconds on the way. A double click is two clicks on one card inside 300
|
|
77
|
+
ms of that time. Two tests hold the port to the page:
|
|
78
|
+
|
|
79
|
+
- **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
|
|
80
|
+
unmodified on a page that does nothing, with a seeded `Math.random` and a
|
|
81
|
+
clock of its own. It plays 40 games through the `Solitaire` class the way
|
|
82
|
+
the page's clicks reach it: the stock, a card, an empty foundation or
|
|
83
|
+
column, the felt, the menu's pauses and New. Half are a random clicker
|
|
84
|
+
that now and then clicks the same card again quickly. Half are a greedy
|
|
85
|
+
player, which won 16 of its 285 deals. That is 134,958 clicks and clock
|
|
86
|
+
steps. The test makes the same ones through the port and requires the same
|
|
87
|
+
state after every one: every pile, face up or down, the card held and
|
|
88
|
+
where from, moves, score, the seconds on the clock, whether it runs, and
|
|
89
|
+
the win screens set going. Widening the double click to 310 ms fails it at
|
|
90
|
+
seed 1, event 266; a bonus of x36 fails it at seed 2, event 3,194.
|
|
91
|
+
- **`tests/test_rules.py`.** It sets up by hand the cases worth pinning,
|
|
92
|
+
including the fixes below, so that undoing one shows as a change.
|
|
93
|
+
|
|
94
|
+
## Fixed in both
|
|
95
|
+
|
|
96
|
+
- **Only a column's top card goes to a foundation.** The page checked the card
|
|
97
|
+
picked and moved the column's top card: with the 5♥ and 4♠ face up and the
|
|
98
|
+
hearts at 4, picking the 5♥ put the 4♠ on the hearts. Now nothing moves.
|
|
99
|
+
- **A card on a foundation stays there.** Moving one to another foundation
|
|
100
|
+
counted a move and moved nothing; now it counts nothing.
|
|
101
|
+
- **A won table is finished.** It still played: a king taken off its
|
|
102
|
+
foundation and put back won again, with a second bonus and a second score.
|
|
103
|
+
A win now stops the clock for good, and Help and the scores no longer start
|
|
104
|
+
it again.
|
|
105
|
+
- **The bonus is paid on the time to the win,** read at the winning move, not
|
|
106
|
+
on the timer's last tick up to a second before it. A win under 30 seconds is
|
|
107
|
+
paid as 30, so the bonus never falls as a win gets faster: it was 24,500,000
|
|
108
|
+
at one second and nothing under one.
|
|
109
|
+
- **The clock holds for every reason at once.** A modal and the page being
|
|
110
|
+
hidden each hold it, and it runs again only once neither does. A new deal
|
|
111
|
+
starts it clean, and drops a win screen still waiting from the last deal.
|
|
112
|
+
- **A draw puts down a waste card held.** The page kept it held by its old
|
|
113
|
+
place on the waste, so the next move took whatever card the draw had put
|
|
114
|
+
there. Worse, holding the waste's king, turning the waste back into the
|
|
115
|
+
stock and clicking an empty column pushed `undefined` into the column, and
|
|
116
|
+
every render after threw: in Chromium, "Cannot read properties of
|
|
117
|
+
undefined (reading 'getHTML')", and the table stopped answering clicks.
|
|
118
|
+
`drawFromStock` and `draw_from_stock` now put a held waste card down first;
|
|
119
|
+
a tableau card held stays held. In Chromium the same clicks on the fixed
|
|
120
|
+
page leave the column empty, and the king, drawn again, goes there. Taking
|
|
121
|
+
the fix out of the port alone fails the oracle at seed 1, event 252; the
|
|
122
|
+
trace has 57 draws with a waste card held, three of them turn-overs.
|
|
123
|
+
|
|
124
|
+
## Licence
|
|
125
|
+
|
|
126
|
+
PolyForm Noncommercial 1.0.0, as for the rest of this repository. See
|
|
127
|
+
`LICENSE` and `NOTICE`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Solitaire (terminal)
|
|
2
|
+
|
|
3
|
+
The magmacrunch arcade's Solitaire, Klondike draw three, in a terminal. It is
|
|
4
|
+
the second version of 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-solitaire
|
|
11
|
+
solitaire # the title
|
|
12
|
+
solitaire --play --seed 7 # straight to a deal, the same deals every time
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For working on it, from this folder, once the website's `shared/` is linked
|
|
16
|
+
in beside `web/` (`node tools/link-shared.mjs` from the repository's root),
|
|
17
|
+
which the oracle test needs:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install -e ".[dev]"
|
|
21
|
+
python -m klondike
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Keys
|
|
25
|
+
|
|
26
|
+
A cursor stands for the mouse, and ENTER is a click wherever it stands.
|
|
27
|
+
|
|
28
|
+
| key | |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `←` `→` | along the top row (stock, waste, foundations) or across the columns |
|
|
31
|
+
| `↑` `↓` | up and down a column's face-up cards, and between the top row and the columns |
|
|
32
|
+
| `1` to `7` | a column |
|
|
33
|
+
| `ENTER` `SPACE` | click: draw from the stock, pick a card up, or put the held card there |
|
|
34
|
+
| `F` | double-click: a card to its foundation, if it can go |
|
|
35
|
+
| `D` | draw, wherever the cursor is |
|
|
36
|
+
| `X` | click the felt: put the held card down |
|
|
37
|
+
| `N` | deal again |
|
|
38
|
+
| `H` `S` | help, the scores; the clock stops while they are open |
|
|
39
|
+
| `Esc` `Esc` | leave the deal, for the title (and from there, the arcade) |
|
|
40
|
+
| `Q` | quit |
|
|
41
|
+
|
|
42
|
+
The stock shows its back and how many cards are left. The waste shows its
|
|
43
|
+
top three, fanned. A column shows its face-down cards as one back and a
|
|
44
|
+
count, then its face-up cards one to a row. The held card is lit cyan and
|
|
45
|
+
the cursor pink. A win shows the page's win screen: time, moves, score, and
|
|
46
|
+
the time bonus, with a NEW HIGH SCORE badge when the score makes the board.
|
|
47
|
+
It goes on the board with its time beside it, as the page saves it.
|
|
48
|
+
|
|
49
|
+
Needs a 41x24 terminal.
|
|
50
|
+
|
|
51
|
+
## It is the browser's game, on the browser's clock
|
|
52
|
+
|
|
53
|
+
`klondike/rules.py` is `web/js/game.js`, method for method, with the deck and
|
|
54
|
+
its shuffle from the website's `shared/adenosine-cards.js`. The page keeps
|
|
55
|
+
its time with a one-second `setInterval`, and that is part of the rules: the
|
|
56
|
+
time on the table, and the bonus a win is paid, are whatever that timer last
|
|
57
|
+
set. So the port keeps the page's clock too. Its time only moves when the
|
|
58
|
+
screen moves it, by the real time gone, and the interval fires at each of
|
|
59
|
+
its seconds on the way. A double click is two clicks on one card inside 300
|
|
60
|
+
ms of that time. Two tests hold the port to the page:
|
|
61
|
+
|
|
62
|
+
- **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
|
|
63
|
+
unmodified on a page that does nothing, with a seeded `Math.random` and a
|
|
64
|
+
clock of its own. It plays 40 games through the `Solitaire` class the way
|
|
65
|
+
the page's clicks reach it: the stock, a card, an empty foundation or
|
|
66
|
+
column, the felt, the menu's pauses and New. Half are a random clicker
|
|
67
|
+
that now and then clicks the same card again quickly. Half are a greedy
|
|
68
|
+
player, which won 16 of its 285 deals. That is 134,958 clicks and clock
|
|
69
|
+
steps. The test makes the same ones through the port and requires the same
|
|
70
|
+
state after every one: every pile, face up or down, the card held and
|
|
71
|
+
where from, moves, score, the seconds on the clock, whether it runs, and
|
|
72
|
+
the win screens set going. Widening the double click to 310 ms fails it at
|
|
73
|
+
seed 1, event 266; a bonus of x36 fails it at seed 2, event 3,194.
|
|
74
|
+
- **`tests/test_rules.py`.** It sets up by hand the cases worth pinning,
|
|
75
|
+
including the fixes below, so that undoing one shows as a change.
|
|
76
|
+
|
|
77
|
+
## Fixed in both
|
|
78
|
+
|
|
79
|
+
- **Only a column's top card goes to a foundation.** The page checked the card
|
|
80
|
+
picked and moved the column's top card: with the 5♥ and 4♠ face up and the
|
|
81
|
+
hearts at 4, picking the 5♥ put the 4♠ on the hearts. Now nothing moves.
|
|
82
|
+
- **A card on a foundation stays there.** Moving one to another foundation
|
|
83
|
+
counted a move and moved nothing; now it counts nothing.
|
|
84
|
+
- **A won table is finished.** It still played: a king taken off its
|
|
85
|
+
foundation and put back won again, with a second bonus and a second score.
|
|
86
|
+
A win now stops the clock for good, and Help and the scores no longer start
|
|
87
|
+
it again.
|
|
88
|
+
- **The bonus is paid on the time to the win,** read at the winning move, not
|
|
89
|
+
on the timer's last tick up to a second before it. A win under 30 seconds is
|
|
90
|
+
paid as 30, so the bonus never falls as a win gets faster: it was 24,500,000
|
|
91
|
+
at one second and nothing under one.
|
|
92
|
+
- **The clock holds for every reason at once.** A modal and the page being
|
|
93
|
+
hidden each hold it, and it runs again only once neither does. A new deal
|
|
94
|
+
starts it clean, and drops a win screen still waiting from the last deal.
|
|
95
|
+
- **A draw puts down a waste card held.** The page kept it held by its old
|
|
96
|
+
place on the waste, so the next move took whatever card the draw had put
|
|
97
|
+
there. Worse, holding the waste's king, turning the waste back into the
|
|
98
|
+
stock and clicking an empty column pushed `undefined` into the column, and
|
|
99
|
+
every render after threw: in Chromium, "Cannot read properties of
|
|
100
|
+
undefined (reading 'getHTML')", and the table stopped answering clicks.
|
|
101
|
+
`drawFromStock` and `draw_from_stock` now put a held waste card down first;
|
|
102
|
+
a tableau card held stays held. In Chromium the same clicks on the fixed
|
|
103
|
+
page leave the column empty, and the king, drawn again, goes there. Taking
|
|
104
|
+
the fix out of the port alone fails the oracle at seed 1, event 252; the
|
|
105
|
+
trace has 57 draws with a waste card held, three of them turn-overs.
|
|
106
|
+
|
|
107
|
+
## Licence
|
|
108
|
+
|
|
109
|
+
PolyForm Noncommercial 1.0.0, as for the rest of this repository. See
|
|
110
|
+
`LICENSE` and `NOTICE`.
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""``python -m klondike`` -- play the arcade's Solitaire: Klondike, draw three."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="solitaire",
|
|
10
|
+
description="Klondike solitaire, draw three.")
|
|
11
|
+
parser.add_argument("--play", action="store_true", help="skip the title screen")
|
|
12
|
+
parser.add_argument("--seed", type=int, default=None, help="the same deals every time")
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--ascii", action="store_true", dest="ascii_only",
|
|
15
|
+
help="draw with plain ASCII instead of block and box glyphs. Detected "
|
|
16
|
+
"automatically from the terminal's encoding; this forces it. "
|
|
17
|
+
"MAGMACRUNCH_ASCII=1 says the same for every cabinet.",
|
|
18
|
+
)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
from klondike.app import run
|
|
22
|
+
|
|
23
|
+
run(args.seed, args.play, args.ascii_only)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
main()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Wiring -- what outlives one deal, and how the screens reach it.
|
|
2
|
+
|
|
3
|
+
Everything that draws lives in :mod:`klondike.scenes`; everything that decides
|
|
4
|
+
lives in :mod:`klondike.rules`. **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
|
+
import time
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from magmacrunch.engine.scores import ScoreBook
|
|
17
|
+
|
|
18
|
+
from klondike.arcade import INFO
|
|
19
|
+
from klondike.scenes import GameScene, TitleScene
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SolitaireApp:
|
|
23
|
+
def __init__(self, host: Any, seed: int | None = None,
|
|
24
|
+
scores: ScoreBook | None = None,
|
|
25
|
+
clock: Callable[[], float] | None = None):
|
|
26
|
+
self.host = host
|
|
27
|
+
#: Where each deal's shuffle comes from. Seeded, so ``--seed``
|
|
28
|
+
#: replays a whole session.
|
|
29
|
+
self.rng = random.Random(seed)
|
|
30
|
+
#: The board the browser posts to: the score of a won game, with its
|
|
31
|
+
#: time beside it.
|
|
32
|
+
self.scores = scores or ScoreBook(INFO.scoreboard)
|
|
33
|
+
#: Seconds, for the page's clock. A test hands in its own.
|
|
34
|
+
self.clock = clock or time.monotonic
|
|
35
|
+
self.initials = "AAA"
|
|
36
|
+
self.last_rank: int | None = None
|
|
37
|
+
self.root_scene = TitleScene(self)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def renderer(self):
|
|
41
|
+
return self.host.renderer
|
|
42
|
+
|
|
43
|
+
def new_rand(self):
|
|
44
|
+
return random.Random(self.rng.randrange(1 << 32)).random
|
|
45
|
+
|
|
46
|
+
def start_game(self) -> None:
|
|
47
|
+
self.host.push_scene(GameScene(self))
|
|
48
|
+
|
|
49
|
+
def show_scores(self) -> None:
|
|
50
|
+
from klondike.scenes import ScoresScene
|
|
51
|
+
|
|
52
|
+
self.host.push_scene(ScoresScene(self))
|
|
53
|
+
|
|
54
|
+
def show_rules(self) -> None:
|
|
55
|
+
from klondike.scenes import RulesScene
|
|
56
|
+
|
|
57
|
+
self.host.push_scene(RulesScene(self))
|
|
58
|
+
|
|
59
|
+
def enter_initials(self, score: int, extra: dict) -> None:
|
|
60
|
+
from klondike.scenes import InitialsScene
|
|
61
|
+
|
|
62
|
+
self.host.push_scene(InitialsScene(self, score, extra))
|
|
63
|
+
|
|
64
|
+
def to_title(self) -> None:
|
|
65
|
+
self.host.pop_scene()
|
|
66
|
+
|
|
67
|
+
def leave(self) -> None:
|
|
68
|
+
self.host.pop_scene()
|
|
69
|
+
|
|
70
|
+
def qualifies(self, score: int) -> bool:
|
|
71
|
+
"""``isHighScore``: above nothing, and in the top ten."""
|
|
72
|
+
return score > 0 and self.scores.qualifies(score)
|
|
73
|
+
|
|
74
|
+
def record(self, score: int, initials: str | None = None,
|
|
75
|
+
extra: dict | None = None):
|
|
76
|
+
self.initials = initials or self.initials
|
|
77
|
+
result = self.scores.save(self.initials, score, **(extra or {}))
|
|
78
|
+
self.last_rank = result.rank
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def scene(self):
|
|
83
|
+
return self.host.scene
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def run(seed: int | None = None, skip_title: bool = False,
|
|
87
|
+
ascii_only: bool = False) -> None:
|
|
88
|
+
"""Play Solitaire as its own command."""
|
|
89
|
+
from magmacrunch.engine.core.tui_host import TuiHost
|
|
90
|
+
from magmacrunch.engine.ui.glyphs import Glyphs
|
|
91
|
+
|
|
92
|
+
host = TuiHost(title=INFO.title, fps=INFO.fps, hold_ms=INFO.hold_ms,
|
|
93
|
+
glyphs=Glyphs.detect(ascii_only=ascii_only))
|
|
94
|
+
app = SolitaireApp(host, seed)
|
|
95
|
+
host.push_scene(app.root_scene)
|
|
96
|
+
if skip_title:
|
|
97
|
+
host.push_scene(GameScene(app))
|
|
98
|
+
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 klondike import theme
|
|
11
|
+
|
|
12
|
+
INFO = GameInfo(
|
|
13
|
+
key="solitaire",
|
|
14
|
+
title="Solitaire",
|
|
15
|
+
blurb="Klondike, draw three, against the clock.",
|
|
16
|
+
# The frame rate moves the page's clock and the cursor, nothing else.
|
|
17
|
+
# Edge input, so one arrow is one step.
|
|
18
|
+
fps=20,
|
|
19
|
+
hold_ms=0,
|
|
20
|
+
min_cols=theme.MIN_COLS,
|
|
21
|
+
min_rows=theme.MIN_ROWS,
|
|
22
|
+
accent=theme.PINK,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SolitaireGame:
|
|
27
|
+
"""Satisfies :class:`magmacrunch.engine.arcade.ArcadeGame`."""
|
|
28
|
+
|
|
29
|
+
info = INFO
|
|
30
|
+
|
|
31
|
+
def start(self, host: Any) -> Any:
|
|
32
|
+
from klondike.app import SolitaireApp
|
|
33
|
+
|
|
34
|
+
return SolitaireApp(host).root_scene
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
GAME = SolitaireGame()
|
|
38
|
+
|
|
39
|
+
__all__ = ["GAME", "INFO", "SolitaireGame"]
|