diff --git a/.gitignore b/.gitignore index 0edcb84..e2eef37 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,18 @@ /card-games-*-src.tar.gz *.sketch *~ +# Generated documentation (built from doc/card-games.texi with makeinfo) +/doc/card-games.info +/doc/card-games.html +/doc/card-games.pdf +/doc/card-games.aux +/doc/card-games.log +/doc/card-games.toc +/doc/card-games.cp +/doc/card-games.cps +/doc/card-games.fn +/doc/card-games.fns +/doc/card-games.ky +/doc/card-games.pg +/doc/card-games.tp +/doc/card-games.vr diff --git a/Makefile b/Makefile index 638e9a4..9bd7920 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Makefile for card-games -- byte-compile, test, and package. EMACS ?= emacs PKG = card-games -VERSION = 1.0.90 +VERSION = 1.0.91 # Source files in dependency order (cg-core first). EL = cg-core.el cg-svg.el cg-render.el cg-net.el cg-bid.el cg-gaps.el cg-bid-ui.el cg-bid-net.el cg-solitaire.el cg-trick.el cg-eights.el cg-patience.el cg-president.el cg-rummy.el cg-rum500.el cg-handfoot.el cg-match.el cg-cribbage.el cg-scopa.el cg-trick-ext.el cg-spite.el cg-bridge.el cg-crapette.el card-games.el ELC = $(EL:.el=.elc) @@ -11,9 +11,17 @@ TAR = $(TARDIR).tar SRCTAR = $(PKG)-$(VERSION)-src.tar.gz DIST = dist BATCH = $(EMACS) -Q --batch -L . -EXTRA = README.org $(PKGDESC) +MANUAL = doc/card-games.texi +VERTEXI = doc/version.texi +INFO = doc/card-games.info +IMAGES = doc/images/klondike.png doc/images/hearts.png +MAKEINFO ?= makeinfo +TEXI2PDF ?= texi2pdf +README = README.md +EXTRA = README.org $(README) $(PKGDESC) $(MANUAL) $(VERTEXI) $(IMAGES) -.PHONY: all compile test clean distclean checkdoc lint package tarball elpa release help +.PHONY: all compile test clean distclean checkdoc lint package tarball elpa release help \ + info info-emacs html pdf docclean version readme hooks help: @echo "card-games $(VERSION) -- make targets:" @@ -21,6 +29,13 @@ help: @echo " test run the ERT test suite" @echo " checkdoc run checkdoc on all sources" @echo " lint run package-lint (if installed)" + @echo " version regenerate $(VERTEXI) from VERSION" + @echo " readme export README.org -> README.md (Emacs batch)" + @echo " hooks install the git pre-commit hook" + @echo " info build the Info manual ($(INFO)) with makeinfo" + @echo " info-emacs build the Info manual with Emacs alone (fallback)" + @echo " html build the one-file HTML manual" + @echo " pdf build the PDF manual (needs a TeX installation)" @echo " package build the installable package tarball ($(TAR))" @echo " elpa build a one-package ELPA archive in $(DIST)/" @echo " release clean + test + package + source tarball" @@ -66,8 +81,8 @@ elpa: tarball # Source snapshot for a GitHub release. Archive an explicit file list # (not ".") so the growing output tarball and editor lock files are never # read mid-write -- which is what caused "tar: .: file changed as we read it". -SRCFILES = $(EL) $(EXTRA) Makefile .gitignore test -release: distclean test tarball +SRCFILES = $(EL) $(EXTRA) build.el hooks Makefile .gitignore test +release: distclean version readme test tarball rm -f $(SRCTAR) tar --transform 's,^,$(TARDIR)/,' \ --exclude='*.elc' --exclude='*.tar' --exclude='*.tar.gz' \ @@ -75,8 +90,58 @@ release: distclean test tarball -czf $(SRCTAR) $(SRCFILES) @echo "Built $(SRCTAR) and $(TAR) for release $(VERSION)" +# Documentation. The Texinfo source lives in doc/; the Info manual is +# built from it with makeinfo. doc/version.texi carries the version and +# is committed; `make version' regenerates it from VERSION (run at release). +version: + @printf '@set VERSION %s\n@set UPDATED %s\n@set YEAR %s\n' \ + "$(VERSION)" "$$(date '+%-d %B %Y')" "$$(date +%Y)" > $(VERTEXI) + @echo "Wrote $(VERTEXI) (VERSION $(VERSION))" + +# README: Org is the source; GitHub and MELPA render Markdown better than +# Org, so we export README.org -> README.md with Emacs batch (ox-md). The +# git pre-commit hook (make hooks) keeps README.md in step automatically. +readme: $(README) +$(README): README.org build.el + $(BATCH) -l build.el + +# Install the pre-commit hook into this checkout's .git/hooks. +hooks: + @if [ -d .git ]; then \ + cp hooks/pre-commit .git/hooks/pre-commit && \ + chmod +x .git/hooks/pre-commit && \ + echo "Installed .git/hooks/pre-commit"; \ + else \ + echo "No .git directory here; skipping hook install."; \ + fi + +info: $(INFO) +$(INFO): $(MANUAL) $(VERTEXI) + $(MAKEINFO) -o $@ $(MANUAL) + +# Fallback: build the Info manual with Emacs alone (no makeinfo needed). +# Handy on Windows/MSYS2 where Emacs is present but texinfo may not be. +# The native formatter is lower fidelity than makeinfo, so `make info' +# stays the default; this is only for a makeinfo-less environment. +info-emacs: $(MANUAL) $(VERTEXI) + cd doc && $(EMACS) -Q --batch --eval "(require 'texinfmt)" \ + -f batch-texinfo-format card-games.texi + +html: $(MANUAL) $(VERTEXI) + $(MAKEINFO) --html --no-split -o doc/card-games.html $(MANUAL) + +pdf: $(MANUAL) $(VERTEXI) + cd doc && $(TEXI2PDF) -q card-games.texi + +docclean: + rm -f $(INFO) doc/card-games.html doc/card-games.pdf \ + doc/card-games.aux doc/card-games.cp doc/card-games.cps \ + doc/card-games.fn doc/card-games.ky doc/card-games.log \ + doc/card-games.pg doc/card-games.toc doc/card-games.tp \ + doc/card-games.vr doc/card-games.fns + clean: rm -f $(ELC) -distclean: clean +distclean: clean docclean rm -rf $(DIST) $(TARDIR) $(TAR) $(SRCTAR) diff --git a/NEWS b/NEWS index c2f0156..91faeb2 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,18 @@ card-games NEWS -- user-visible changes ======================================== +* Version 1.0.91 (pretest2: playtest) + +** Documentation + - A complete Info manual now ships with the package: ~C-h i~ and choose + *Card Games* (or ~M-x info RET (card-games) RET~). It is built with + makeinfo (~make info~); ~make info-emacs~ builds it with Emacs alone + for environments without a texinfo installation. + - The README is generated from ~README.org~ to ~README.md~ with Emacs + batch (~ox-md~), so GitHub and MELPA render it faithfully; a + ~pre-commit~ hook (~make hooks~) keeps ~README.md~ in step. Added + board screenshots and refreshed the game list and customization notes. + * Version 1.0.90 (pre-test snapshot) This release grows the package from the five-game 1.0.60 candidate to a @@ -39,6 +51,16 @@ the mouse as well as the keyboard. the middle, and your fanned hand, with the legal cards you may play ringed. Click a card to play it. Set ~cg-trick-svg-cards~ to nil for the plain-text board. + - The Emacs emblem in the full-window (SVG) UIs is now switchable via + ~cg-svg-emacs-logo~: it embeds a real logo that ships with Emacs -- + the modern icon (default), the classic icon, a GNU head, or the + splash image -- or the small built-in drawn emblem, or none. + - More card backs, and a random default. ~cg-svg-card-back~ now offers + ~dots~, ~rings~, ~solid~, ~lattice~, ~waves~, ~diamond~, and four + backs stamped with an Emacs logo (~emacs~, ~emacs-classic~, ~gnu~, + ~splash~). It defaults to ~random~, which picks a back for the + session; ~M-x cg-svg-shuffle-card-back~ (or reopening the game menu) + rolls a new one. - Full SVG board for the rummy games (Gin Rummy, Rummy, Rummy 500): the stock and discard, the melds already down on the table, and your fanned hand with cursor, marks, and lay-off hints; click a card to @@ -46,6 +68,15 @@ the mouse as well as the keyboard. - The same board now covers Hand & Foot (each team's books, and your hand or foot) and the fishing games Scopa and Casino (the loose table cards and the deck). + - SVG boards for Crazy Eights (the discard, the suit in play, and the + stock), Spite & Malice (the four centre piles, your goal and discard + piles), and Old Maid (the opponents and your hand; click one of the + next player's face-down cards to draw it). + - SVG boards for Cribbage (a peg-track for each player, the starter, the + pegging count and cards, and the crib at the show) and Bridge (the + rubber and contract, all four seats with the dummy exposed, the trick + in the middle, and the hand you are playing). With these every game + in the collection now has a full graphical board. ** Rummy-family rules completed - Rummy 500: take a card from anywhere in the discard pile (key ~T~) -- @@ -62,6 +93,30 @@ the mouse as well as the keyboard. "Next hand" button and the hand fan. - Live multiplayer 500 over TCP (~M-x cg-bid-host~ / ~M-x cg-bid-join~). +** Pick how the games are drawn + - From the ~M-x card-game~ menu (or ~M-x card-games-set-treatment~) you + can switch all the games between ~text~ (UNICODE cards), ~svg~ (drawn + cards), and ~full~ (the full-window SVG table where a game has one, + Gaps and 500). + +** Adjustable computer opponents + - A new ~cg-ai-level~ (easy, normal, hard) sets how hard the computer + plays, and you can change it right from the ~M-x card-game~ menu (or + with ~M-x card-games-set-ai-level~). Russian Bank (Crapette) plays + all three levels; the trick-taking games play a random legal card on + easy. Other games ignore it for now. + +** A craftier Russian Bank (Crapette) opponent + - On normal and hard the computer empties its reserve first (the pile + you must clear to win) and prefers loading its cards onto you; on hard + it also looks a move ahead to rearrange the houses when doing so frees + a card it could not otherwise place. Easy keeps the old simple play. + +** Bug fixes + - Alternating-colour building (the tableau solitaires and Russian Bank) + wrongly treated a diamond and a heart as opposite colours, so a red + card could be built on another red card. Both are red; fixed. + ** Playtest fixes - ~q~ now returns to the ~M-x card-game~ menu from any game, instead of burying the buffer and leaving the previous buffer on screen. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3644c7d --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +Card games for Emacs: about thirty of them, from Klondike and FreeCell +to Hearts, 500, Gin, Cribbage, and two-player Russian Bank against the +computer. Every game plays with the keyboard everywhere and with the +mouse on a graphical display. + +![img](doc/images/klondike.png) + +On a graphical display the cards are drawn as SVG; in a terminal they +fall back to UNICODE glyphs (customize `card-game-symbols`). You can +switch how every game is drawn from the menu – `text` (UNICODE), +`svg` (drawn cards), or `full` (a full-window SVG table) – and dial the +computer opponents between `easy`, `normal`, and `hard`. + +A full Info manual ships with the package: after installing, `C-h i` and +choose **Card Games**, or `M-x info RET (card-games) RET`. + + +# Games + +To open the game menu type `M-x card-game`, or start a game directly +with its command. From the menu you can also switch the card treatment +(text / SVG / full-window) and the AI difficulty. + + +## Trick-taking + +- `cg-bid` – 500 (Bid). Win the auction, name the trump suit, then take + tricks with your partner to reach 500 points before the opposing pair. + Also playable live over the network (`M-x cg-bid-host` / `cg-bid-join`). +- `cg-hearts` – Hearts. Avoid taking hearts and the Queen of Spades, or + take them all to "shoot the moon"; lowest score loses. +- `cg-spades` – Spades. Partnership bidding to 500; spades are always + trump. Make your side's combined bid, mind the bags, dare a nil. +- `cg-whist` – Whist. Trump is the turned card, there is no bidding; + score one point for each trick past the book of six. +- `cg-ohhell` – Oh Hell. The hand shrinks each round; bid the exact + number of tricks you will take, no more and no fewer. + + +## Solitaire + +- `cg-montana` – Montana (also called Gaps). Each row is anchored by a + Two and built upward in one suit, 2 through King; slide cards into the + gaps until all four rows are sorted. +- `cg-gaps` – an alias for `cg-montana`. +- `cg-hells-half-acre` – the build-down variant: each row is anchored by + a King and built downward, King through 2. +- `cg-klondike` – Klondike, the classic "Solitaire": build the four + foundations up by suit from the Ace. +- `cg-freecell` – FreeCell: every card in view, four free cells, a game + of nearly pure skill. +- `cg-spider` – Spider (two decks): build down regardless of suit, but + only same-suit runs move; clear eight King-to-Ace runs. +- `cg-yukon` – Yukon: Klondike's layout dealt mostly face up, with any + buried group movable and no stock. +- `cg-canfield` – Canfield: a 13-card reserve and a foundation base rank + set by the deal; foundations wrap King to Ace. +- `cg-forty-thieves` – Forty Thieves: two decks, ten columns, eight + foundations, build down by suit, and no second pass through the stock. +- `cg-scorpion` – Scorpion: build down by suit and free any buried group + to assemble four King-to-Ace runs. +- `cg-golf` – Golf: clear the layout by playing exposed cards one rank + above or below the waste top. +- `cg-tripeaks` – TriPeaks: the same, on three overlapping peaks, with + Ace-King wrapping for long chains. +- `cg-pyramid` – Pyramid: remove pairs of exposed cards whose ranks sum + to thirteen; Kings go alone. + + +## Shedding and climbing + +- `cg-eights` – Crazy Eights. Match the suit or rank of the discard; + eights are wild and let you name the next suit. +- `cg-president` – President (Scum). Climb: play one to four of a rank, + beat it or pass; first out rules, last out scrubs, and the roles trade + cards on the next deal. + + +## Rummy + +- `cg-gin` – Gin Rummy. A two-handed duel: draw or take the discard, + build sets and runs, and knock once your deadwood is ten or less, or go + gin with none; your opponent then lays off and may undercut you. First + to 100 wins. +- `cg-rummy-basic` – Rummy. Meld sets and runs onto the table and lay + cards off onto them; empty your hand to go out and score the cards left + in the other hands. +- `cg-rum500` – Rummy 500. As above, but you score the cards you lay + down and lose the cards left in your hand; first past 500 wins. Take a + buried discard card with `T`: you take it and every card above it, and + meld the chosen card at once. +- `cg-handfoot` – Hand & Foot. A partnership Canasta cousin: play a hand + and then a foot, build books of a rank with Twos and Jokers wild, and go + out once your side has completed two of them. Each round opens with a + rising go-down minimum (50, 90, 120, 150); red threes are bonus cards; + and you can pick up the discard pile (`p`) by melding its top card with + two matching naturals. + + +## Matching + +- `cg-go-fish` – Go Fish. Ask another player for a rank you hold; + collect all four to lay down a book, and make the most books. +- `cg-old-maid` – Old Maid. One Queen is set aside; discard pairs and + draw blind from your neighbour, and do not be left with the odd Queen. + + +## Pegging + +- `cg-cribbage` – Cribbage. Lay two cards to the crib, cut a starter, + peg toward 31, then count fifteens, pairs, runs, flushes, and his nobs. + Two-handed to 121. + + +## Capturing + +- `cg-scopa` – Scopa. A 40-card deck; capture table cards by value and + sweep the board for a scopa. Score cards, coins, the sette bello, and + primiera to 11. +- `cg-casino` – Casino. The full deck; capture by pairs and sums and + score cards, spades, the casinos, and aces to 21. + + +## More trick-taking + +- `cg-euchre` – Euchre. A 24-card deck with the two bowers; order up or + call trump and take three of five tricks. Partnership to 10. +- `cg-pitch` – Auction Pitch. Bid for the pitch; your first lead sets + trump. Score High, Low, Jack, and Game; first to 7. +- `cg-briscola` – Briscola. A fixed trump turned from the deal and no + obligation to follow suit; capture the Aces and Threes. Partnership to + 61 of the 120 points. + + +## Climbing patience + +- `cg-spite` – Spite & Malice. Race the computer to empty your goal + pile onto shared centre piles that build Ace to Queen; Kings are wild. + + +## Bridge + +- `cg-bridge` – Contract Bridge. A full auction (bids, pass, double, + redouble), play with the dummy exposed, and classic rubber scoring with + vulnerability. You are South; when you declare you play the dummy too. + The bidding AI is a small natural system, sensible but no expert. + + +## Two-player patience + +- `cg-russian-bank` / `cg-crapette` – Russian Bank (Crapette). A race + against the computer: build the eight shared foundations up by suit, + build the shared houses down in alternating colour, and empty your + reserve to win. Load your cards onto your opponent's piles when the + rank and suit line up, mind the "stop" rule that ends your turn if you + skip a foundation play, and sequence house-to-house moves within the + free houses (`[` / `]` choose how many cards of a run to drop on an + empty house). The opponent plays all three difficulty levels. + + +# TODO + +- [X] make the suit symbols customizable (`cg-symbols`) and obey them +- [X] a Texinfo manual +- [ ] finish `checkdoc` docstrings across the per-game files + (the shared engine files are clean; `make compile` is warning-free) +- [ ] renderer "skins": let games subclass the display components (text, + SVG, full-window SVG) +- [X] a manual card-size control for the full-window SVG UI +- [ ] more games + + +# Install + + +## From the package tarball + + make package # builds card-games-1.0.91.tar + +Then in Emacs: `M-x package-install-file RET card-games-1.0.91.tar`. + + +## With `use-package` + +Once the package is on your `load-path` (installed from the tarball or an +ELPA archive), the whole collection loads from the single `card-games` +feature: + + (use-package card-games + :commands (card-game cg-klondike cg-bid cg-hearts cg-gin cg-crapette)) + +Then `M-x card-game` for the menu. + + +## From a local ELPA archive + + make elpa # builds dist/ (archive-contents + tar) + + (add-to-list 'package-archives '("cg" . "/path/to/dist/")) + (package-refresh-contents) + (package-install 'card-games) + + +## Manually + +Put the `cg-*.el` and `card-games.el` files on your `load-path` and +`(require 'card-games)`. + + +# Manual + +A complete Info manual is included. After installing, `C-h i` and pick +**Card Games**, or `M-x info RET (card-games) RET`. To build it from a +source checkout: + + make info # builds doc/card-games.info with makeinfo + +The manual text is licensed CC-BY-4.0; the code is GPL-3.0-or-later. + + +# Playing + +Every game works with the keyboard everywhere and with the mouse on a +graphical display. + +- 500: `b` bid, `p` pass, arrows + `RET` to play (or click a card), + `n` next hand / new game, `?` help. +- Gaps: arrows to move (or `hjkl` when `cg-keys` is `classic`), `RET` to + fill a gap (or click it), `r` redeal, `u` undo, `n` new, `?` help. +- Klondike / FreeCell / Spider / Yukon: arrows move between piles, `RET` + picks up a movable run and drops it, `f` sends a card to a foundation, + `a` auto-plays everything it can, `u` undo, `n` new, `?` help. On the + stock pile, `RET` deals or recycles. +- Hearts / Spades: arrows choose a card, `RET` plays it (in Hearts, `RET` + marks a card to pass and `p` sends the three), `n` new match, `?` help. +- Crazy Eights: arrows choose, `RET` plays, `d` draws, `x` passes, `n` + new deal, `?` help. + +On a graphical display, `v` toggles the full-window SVG table. A +**Card size** slider (and the `+` / `-` / `0` keys) resizes the cards, and +in 500 the `? Help / Rules` button explains play. Every game accepts the +mouse: click cards, board slots, buttons, and the slider. + +![img](doc/images/hearts.png) + + +# Testing + +This is a 1.0.91 pre-test snapshot. To try it: + +1. `make compile && make test` – should be warning-free and all green. +2. `M-x card-game` opens the menu, or jump straight in, e.g. + `M-x cg-klondike`, `M-x cg-bid` (500), `M-x cg-gin`, `M-x cg-handfoot`. +3. On a graphical display, press `v` in 500 for the full-window SVG + table and play entirely with the mouse: click a bid, click five kitty + cards and the **Discard** button, click cards to play, move the **Card + size** slider, and open `? Help / Rules`. + +Feedback most wanted: anything a mouse-only player who is new to Emacs +finds confusing or unreachable, rules bugs, and rendering glitches. + + +# Customization + +`M-x customize-group RET cg-svg` and `RET card-games`: + +- `cg-ai-level` – how hard the computer plays: `easy`, `normal`, or + `hard` (also on the `M-x card-game` menu, or `M-x + card-games-set-ai-level`). +- `card-games-treatment` – how the games are drawn: `text`, `svg`, or + `full` (also on the menu, or `M-x card-games-set-treatment`). +- `cg-svg-theme-colors` – derive the highlight ring and card backs + from your theme (on by default). +- `cg-svg-highlight-color` – the cursor/selection ring (gold by default). +- `cg-bid-felt-color` – the 500 table felt. +- `cg-svg-card-width`, `cg-svg-card-height`, `cg-svg-card-shadow`, + `cg-svg-font-family` – card appearance. +- `cg-svg-card-back` – the card-back design: `dots`, `rings`, `solid`, + `lattice`, `waves`, `diamond`, an Emacs-logo back (`emacs`, + `emacs-classic`, `gnu`, `splash`), or `random` (the default – picks + one for the session; `M-x cg-svg-shuffle-card-back` rolls a new one). +- `cg-svg-emacs-logo` – the emblem on the full-window table: + `modern` (default), `classic`, `gnu`, `splash`, `drawn`, or `none`. +- `cg-symbols` – the Unicode suit glyphs (and the joker) drawn on cards. +- `cg-svg-four-color` – draw a four-colour deck (clubs green, diamonds + blue-purple). +- `cg-keys` – `emacs` (default) or `classic` (adds vi-style `hjkl` and + `SPC`). +- `cg-bid-animate`, `cg-bid-ai-delay`, `cg-bid-trick-pause` – pace the + 500 AI so play is watchable and completed tricks linger. +- `M-x card-games-set-theme` – apply a preset (classic, dark, contrast). + + +# Development + + make compile # byte-compile (should be warning-free) + make test # run the ERT suite + make checkdoc # documentation lint + make release # clean + test + package + source tarball + + +# License + +GPL-3.0-or-later. See the file headers; add a COPYING file with the +full GPLv3 text for distribution. + diff --git a/README.org b/README.org index 9a939c7..900cd84 100644 --- a/README.org +++ b/README.org @@ -1,16 +1,28 @@ #+TITLE: card-games -- Play card games in Emacs #+AUTHOR: Corwin Brust +#+OPTIONS: toc:nil -Card games for Emacs. +Card games for Emacs: about thirty of them, from Klondike and FreeCell +to Hearts, 500, Gin, Cribbage, and two-player Russian Bank against the +computer. Every game plays with the keyboard everywhere and with the +mouse on a graphical display. -Renders SVG by default when ~display-graphic-p~ is t and rsvg is -available. The default (UNICODE) symbols maybe customized by -configuring ~card-game-symbols~. +[[file:doc/images/klondike.png]] + +On a graphical display the cards are drawn as SVG; in a terminal they +fall back to UNICODE glyphs (customize ~card-game-symbols~). You can +switch how every game is drawn from the menu -- ~text~ (UNICODE), +~svg~ (drawn cards), or ~full~ (a full-window SVG table) -- and dial the +computer opponents between ~easy~, ~normal~, and ~hard~. + +A full Info manual ships with the package: after installing, ~C-h i~ and +choose *Card Games*, or ~M-x info RET (card-games) RET~. * Games To open the game menu type ~M-x card-game~, or start a game directly -with its command. +with its command. From the menu you can also switch the card treatment +(text / SVG / full-window) and the AI difficulty. ** Trick-taking - ~cg-bid~ -- 500 (Bid). Win the auction, name the trump suit, then take @@ -116,9 +128,19 @@ with its command. vulnerability. You are South; when you declare you play the dummy too. The bidding AI is a small natural system, sensible but no expert. +** Two-player patience +- ~cg-russian-bank~ / ~cg-crapette~ -- Russian Bank (Crapette). A race + against the computer: build the eight shared foundations up by suit, + build the shared houses down in alternating colour, and empty your + reserve to win. Load your cards onto your opponent's piles when the + rank and suit line up, mind the "stop" rule that ends your turn if you + skip a foundation play, and sequence house-to-house moves within the + free houses (~[~ / ~]~ choose how many cards of a run to drop on an + empty house). The opponent plays all three difficulty levels. + * TODO - [X] make the suit symbols customizable (~cg-symbols~) and obey them -- [ ] a Texinfo manual +- [X] a Texinfo manual - [ ] finish ~checkdoc~ docstrings across the per-game files (the shared engine files are clean; ~make compile~ is warning-free) - [ ] renderer "skins": let games subclass the display components (text, @@ -129,9 +151,19 @@ with its command. * Install ** From the package tarball #+begin_src -make package # builds card-games-1.0.90.tar +make package # builds card-games-1.0.91.tar #+end_src -Then in Emacs: ~M-x package-install-file RET card-games-1.0.90.tar~. +Then in Emacs: ~M-x package-install-file RET card-games-1.0.91.tar~. + +** With ~use-package~ +Once the package is on your ~load-path~ (installed from the tarball or an +ELPA archive), the whole collection loads from the single ~card-games~ +feature: +#+begin_src emacs-lisp +(use-package card-games + :commands (card-game cg-klondike cg-bid cg-hearts cg-gin cg-crapette)) +#+end_src +Then ~M-x card-game~ for the menu. ** From a local ELPA archive #+begin_src @@ -147,8 +179,17 @@ make elpa # builds dist/ (archive-contents + tar) Put the ~cg-*.el~ and ~card-games.el~ files on your ~load-path~ and ~(require 'card-games)~. +* Manual +A complete Info manual is included. After installing, ~C-h i~ and pick +*Card Games*, or ~M-x info RET (card-games) RET~. To build it from a +source checkout: +#+begin_src +make info # builds doc/card-games.info with makeinfo +#+end_src +The manual text is licensed CC-BY-4.0; the code is GPL-3.0-or-later. + * Playing -Both games work with the keyboard everywhere and with the mouse on a +Every game works with the keyboard everywhere and with the mouse on a graphical display. - 500: ~b~ bid, ~p~ pass, arrows + ~RET~ to play (or click a card), @@ -169,8 +210,10 @@ On a graphical display, ~v~ toggles the full-window SVG table. A in 500 the ~? Help / Rules~ button explains play. Every game accepts the mouse: click cards, board slots, buttons, and the slider. +[[file:doc/images/hearts.png]] + * Testing -This is a 1.0.90 pre-test snapshot. To try it: +This is a 1.0.91 pre-test snapshot. To try it: 1. ~make compile && make test~ -- should be warning-free and all green. 2. ~M-x card-game~ opens the menu, or jump straight in, e.g. ~M-x cg-klondike~, ~M-x cg-bid~ (500), ~M-x cg-gin~, ~M-x cg-handfoot~. @@ -184,12 +227,23 @@ finds confusing or unreachable, rules bugs, and rendering glitches. * Customization ~M-x customize-group RET cg-svg~ and ~RET card-games~: +- ~cg-ai-level~ -- how hard the computer plays: ~easy~, ~normal~, or + ~hard~ (also on the ~M-x card-game~ menu, or ~M-x + card-games-set-ai-level~). +- ~card-games-treatment~ -- how the games are drawn: ~text~, ~svg~, or + ~full~ (also on the menu, or ~M-x card-games-set-treatment~). - ~cg-svg-theme-colors~ -- derive the highlight ring and card backs from your theme (on by default). +- ~cg-svg-highlight-color~ -- the cursor/selection ring (gold by default). - ~cg-bid-felt-color~ -- the 500 table felt. - ~cg-svg-card-width~, ~cg-svg-card-height~, ~cg-svg-card-shadow~, ~cg-svg-font-family~ -- card appearance. -- ~cg-svg-card-back~ -- card-back pattern: dots, rings, or solid. +- ~cg-svg-card-back~ -- the card-back design: ~dots~, ~rings~, ~solid~, + ~lattice~, ~waves~, ~diamond~, an Emacs-logo back (~emacs~, + ~emacs-classic~, ~gnu~, ~splash~), or ~random~ (the default -- picks + one for the session; ~M-x cg-svg-shuffle-card-back~ rolls a new one). +- ~cg-svg-emacs-logo~ -- the emblem on the full-window table: + ~modern~ (default), ~classic~, ~gnu~, ~splash~, ~drawn~, or ~none~. - ~cg-symbols~ -- the Unicode suit glyphs (and the joker) drawn on cards. - ~cg-svg-four-color~ -- draw a four-colour deck (clubs green, diamonds blue-purple). diff --git a/build.el b/build.el new file mode 100644 index 0000000..3e90ead --- /dev/null +++ b/build.el @@ -0,0 +1,77 @@ +;;; build.el --- Batch Org -> Markdown export for card-games -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Corwin Brust +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; A very small Emacs-batch exporter. It renders the project's Org +;; sources to Markdown siblings (foo.org -> foo.md) using the built-in +;; `ox-md' backend, so GitHub and MELPA -- which render Markdown more +;; faithfully than Org -- can display them. +;; +;; Run it by hand, from the Makefile, or from the git pre-commit hook: +;; +;; emacs -Q --batch -l build.el +;; +;; By default it exports the files named in `build-org-files' (README.org). +;; Set the CARD_GAMES_ORG environment variable to a space-separated list +;; to override that, e.g. to export more documents. It never touches +;; known-games.org (an internal research list) unless you ask for it. + +;;; Code: + +(require 'org) +(require 'ox-md) + +;; Keep batch mode from blocking on prompts. +(setq org-confirm-babel-evaluate nil + org-export-show-temporary-export-buffer nil + ;; A stray or not-yet-committed image link must never abort the run. + org-export-with-broken-links 'mark + make-backup-files nil) + +(defvar build-org-files '("README.org") + "Default list of Org files to export to Markdown. +Overridden by the CARD_GAMES_ORG environment variable when set.") + +(defvar build-inhibit-run nil + "When non-nil, loading build.el defines helpers but does not export. +ERT or an interactive session can bind this to exercise the helpers.") + +(defun build--targets () + "Return the list of Org files to export. +Honours the CARD_GAMES_ORG environment variable; falls back to +`build-org-files'." + (let ((env (getenv "CARD_GAMES_ORG"))) + (if (and env (not (string-empty-p (string-trim env)))) + (split-string (string-trim env) "[ \t\n]+" t) + build-org-files))) + +(defun build--export-one (orgfile) + "Export ORGFILE to a Markdown sibling. +Log the outcome; never signal, so one bad file cannot abort the run." + (cond + ((not (file-readable-p orgfile)) + (message "build: SKIP %s (not readable)" orgfile)) + (t + (message "build: exporting %s -> markdown" orgfile) + (with-current-buffer (find-file-noselect orgfile) + (condition-case err + (let ((out (org-md-export-to-markdown))) + (message "build: wrote %s" out)) + (error + (message "build: ERROR exporting %s: %s" + orgfile (error-message-string err)))))))) + +(defun build--run () + "Export every file in `build--targets' to Markdown." + (dolist (orgfile (build--targets)) + (build--export-one orgfile)) + (message "build: done")) + +(unless (bound-and-true-p build-inhibit-run) + (build--run)) + +(provide 'build) +;;; build.el ends here diff --git a/card-games-pkg.el b/card-games-pkg.el index 3a3cfa3..9439e5b 100644 --- a/card-games-pkg.el +++ b/card-games-pkg.el @@ -1,5 +1,5 @@ ;;; card-games-pkg.el --- Package metadata -*- no-byte-compile: t; -*- -(define-package "card-games" "1.0.90" +(define-package "card-games" "1.0.91" "Play card games in Emacs (console UNICODE and graphical SVG)." '((emacs "26.1")) :keywords '("games") diff --git a/card-games.el b/card-games.el index 5b55d2d..ded9abc 100644 --- a/card-games.el +++ b/card-games.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -134,6 +134,53 @@ "Registry of playable games. Each entry is (NAME COMMAND DESCRIPTION); `card-game' lists them.") +(defvar card-games--svg-card-vars + '(cg-sol-svg-cards cg-trick-svg-cards cg-rummy-svg-cards cg-eights-svg-cards + cg-bridge-svg-cards cg-crapette-svg-cards cg-pat-svg-cards cg-pres-svg-cards) + "Per-game SVG-cards toggles that `card-games-set-treatment' flips together.") + +(defvar card-games--full-svg-vars + '(cg-gaps-svg-ui cg-bid-svg-ui) + "Full-window SVG toggles (Gaps and 500) used by the `full' treatment.") + +(defvar card-games-treatment 'svg + "Display treatment chosen from the menu: `text', `svg', or `full'.") + +;;;###autoload +(defun card-games-set-treatment (treatment) + "Set how games are drawn: `text' (UNICODE), `svg' (cards), or `full'. +`full' also uses the full-window SVG table where a game has one (Gaps and +500). Takes effect the next time a game is drawn -- press g to redraw an +open game. Gaps and 500 are always graphical on a window system." + (interactive + (list (intern (completing-read "Treatment: " '("text" "svg" "full") nil t)))) + (setq card-games-treatment treatment) + (let ((cards (and (memq treatment '(svg full)) t)) + (full (and (eq treatment 'full) t))) + (dolist (v card-games--svg-card-vars) (when (boundp v) (set v cards))) + (dolist (v card-games--full-svg-vars) (when (boundp v) (set v full)))) + (when (called-interactively-p 'interactive) + (message "Display treatment: %s" treatment))) + +(defun card-game--cycle-treatment (_button) + "Cycle the display treatment and refresh the chooser." + (card-games-set-treatment + (pcase card-games-treatment ('text 'svg) ('svg 'full) (_ 'text))) + (card-game)) + +(defun card-game--cycle-ai (_button) + "Cycle the AI difficulty (`cg-ai-level') and refresh the chooser." + (setq cg-ai-level (pcase cg-ai-level ('easy 'normal) ('normal 'hard) (_ 'easy))) + (card-game)) + +;;;###autoload +(defun card-games-set-ai-level (level) + "Set the computer-opponent difficulty to LEVEL (easy, normal, or hard)." + (interactive + (list (intern (completing-read "AI level: " '("easy" "normal" "hard") nil t)))) + (setq cg-ai-level level) + (message "AI level: %s" level)) + (defun card-game--launch (button) "Start the game whose command is stored on BUTTON." (let ((cmd (button-get button 'card-game-command))) @@ -159,6 +206,9 @@ Each entry is (NAME COMMAND DESCRIPTION); `card-game' lists them.") "Open a chooser listing the available card games. Press RET (or click) on a game to start it." (interactive) + (when (and (boundp 'cg-svg-card-back) (eq cg-svg-card-back 'random) + (fboundp 'cg-svg--roll-back)) + (cg-svg--roll-back)) ; a fresh random back per menu visit (let ((buf (get-buffer-create "*Card Games*"))) (with-current-buffer buf (card-game-mode) @@ -166,8 +216,22 @@ Press RET (or click) on a game to start it." (erase-buffer) (insert (propertize " Card Games for Emacs\n" 'face 'bold)) (insert (propertize - " Choose a game with RET or the mouse. q to quit.\n\n" + " Choose a game with RET or the mouse. q to quit.\n" 'face 'shadow)) + (insert " AI opponents: ") + (insert-text-button + (symbol-name cg-ai-level) + 'face 'link + 'help-echo "Click to change the AI difficulty (easy/normal/hard)" + 'action #'card-game--cycle-ai) + (insert (propertize " (click to cycle easy/normal/hard)\n" 'face 'shadow)) + (insert " Cards: ") + (insert-text-button + (symbol-name card-games-treatment) + 'face 'link + 'help-echo "Click to cycle the display: text / svg / full" + 'action #'card-game--cycle-treatment) + (insert (propertize " (click to cycle text/svg/full)\n\n" 'face 'shadow)) (dolist (g card-games-list) (insert " ") (insert-text-button diff --git a/cg-bid-net.el b/cg-bid-net.el index 9367a97..3ad484c 100644 --- a/cg-bid-net.el +++ b/cg-bid-net.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-bid-ui.el b/cg-bid-ui.el index 6803f80..19412cf 100644 --- a/cg-bid-ui.el +++ b/cg-bid-ui.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -842,14 +842,9 @@ FS scales the N/S/E/W label fonts." (svg-circle svg cx cy 3 :fill "#cfeccf"))) (defun cg-bid--draw-logo (svg cx cy &optional fs) - "Draw a GNU Emacs emblem centred at CX, CY on SVG, scaled by FS." - (let ((fs (or fs 1.0))) - (svg-gradient svg "cg-logo" 'linear '((0 . "#8056c8") (100 . "#3f1f9e"))) - (svg-circle svg cx cy (round (* 26 fs)) :gradient "cg-logo" - :stroke "#2a1370" :stroke-width 2) - (cg-svg--text svg "e" cx (+ cy (round (* 10 fs))) (round (* 30 fs)) "#ffffff" t) - (cg-svg--text svg "GNU Emacs" cx (+ cy (round (* 42 fs))) - (max 10 (round (* 11 fs))) "#c7bbe6"))) + "Draw the configured Emacs emblem centred at CX, CY on SVG, scaled by FS. +The emblem is chosen with `cg-svg-emacs-logo'." + (cg-svg-draw-logo svg cx cy fs)) (defun cg-bid--grid-cell (bid gx gy cw ch g) "Return (X Y W H) for BID in a grid at GX,GY with cells CW by CH, gutter G. diff --git a/cg-bid.el b/cg-bid.el index 979a20a..0ea82cf 100644 --- a/cg-bid.el +++ b/cg-bid.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-bridge.el b/cg-bridge.el index 2d6feb8..31ac37a 100644 --- a/cg-bridge.el +++ b/cg-bridge.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -498,8 +498,108 @@ vulnerability, and TRICKS the declarer side's trick count. Keys: (max 0 (- cg-svg-card-width 26)) 0) :region-tag region-tag)) +(defun cg-bridge--draw-backs (svg x y n) + "Draw up to three overlapped backs at X, Y for a hand of N cards." + (let ((k (min (max n 0) 3)) (xx x)) + (dotimes (_ k) (cg-svg-card svg xx y :down t) (setq xx (+ xx 16))))) + +(defun cg-bridge--svg (game) + "Return an SVG board for the Bridge GAME (four seats, dummy exposed)." + (let* ((w cg-svg-card-width) (h cg-svg-card-height) (gap cg-svg-card-gap) (pad 16) + (phase (cg-get game :phase)) (cursor (cg-get game :cursor)) + (turn (cg-get game :turn)) (dummy (cg-get game :dummy)) + (exposed (cg-get game :exposed)) (trick (cg-get game :trick)) + (act (if (and (eq phase 'play) (memq turn (cg-bridge--controls game))) turn 0)) + (ahand (cg-bridge--sort (cg-bridge--hand game act))) + (n (length ahand)) + (overlap (cond ((> n 11) (- w 26)) ((> n 8) 20) (t 0))) + (step (max 14 (- (+ w gap) overlap))) + (fanw (if (> n 0) (+ (* (1- n) step) w) w)) + (width (max (+ fanw (* 2 pad)) 760)) + (cx (/ width 2)) + (y-title 6) (y-info 24) (y-north 62) + (y-tn (+ y-north h 20)) + (cyc (+ y-tn (round (* h 0.5)))) + (y-ts (+ cyc (round (* h 0.15)))) + (y-hand (+ y-ts h 42)) + (height (+ y-hand h 30)) + (svg (svg-create width height)) + (lc (cg-color 'shadow :foreground "gray50")) + (regions '())) + (cl-labels + ((txt (str x y &optional sz bold) + (apply #'svg-text svg str :x x :y y :font-size (or sz 12) :fill lc + :font-family cg-svg-font-family (and bold '(:font-weight "bold")))) + (seat (s x y) + (if (and exposed (eql s dummy) (/= s act)) + (let ((cs (cg-bridge--sort (cg-bridge--hand game s))) (xx x)) + (dolist (c cs) + (let ((sp (cg-bridge--spec c))) + (cg-svg-card svg xx y :rank (car sp) :suit (cdr sp))) + (setq xx (+ xx 15)))) + (cg-bridge--draw-backs svg x (+ y 6) (length (cg-bridge--hand game s)))) + (txt (format "%s%s%s" (aref cg-bridge-seat-names s) + (if (eql s dummy) " (dummy)" "") + (if (= turn s) " <-" "")) + x y 11)) + (trick-card (s x y) + (let ((play (assq s trick))) + (when play + (let ((sp (cg-bridge--spec (cdr play)))) + (cg-svg-card svg x y :rank (car sp) :suit (cdr sp))))))) + (txt "Bridge" pad (+ y-title 12) 13 t) + (txt (format "Games N-S %d E-W %d Below %d/%d Above %d/%d" + (aref (cg-get game :games) 0) (aref (cg-get game :games) 1) + (aref (cg-get game :below) 0) (aref (cg-get game :below) 1) + (aref (cg-get game :above) 0) (aref (cg-get game :above) 1)) + pad (+ y-info 8) 11) + (pcase phase + ('auction + (txt (format "Auction: %s" (cg-bridge--auction-string game)) pad (+ y-info 24) 11) + (txt (format "Your bid: %d %s (arrows compose, RET bids)" + (cg-get game :bid-level) + (aref cg-bridge-strains (cg-get game :bid-strain))) + pad (+ y-info 40) 11)) + ((or 'play 'scored 'passed-out) + (txt (format "Contract: %s by %s Declarer tricks: %d" + (cg-bridge--contract-string game) + (if (cg-get game :declarer) + (aref cg-bridge-seat-names (cg-get game :declarer)) "--") + (cg-get game :tricks)) + pad (+ y-info 24) 11))) + (seat 2 (- cx 40) y-north) + (seat 1 pad cyc) + (seat 3 (- width pad 110) cyc) + (when (eq phase 'play) + (trick-card 2 (- cx (/ w 2)) y-tn) + (trick-card 0 (- cx (/ w 2)) y-ts) + (trick-card 1 (- cx w (round (* w 0.4))) (round (- cyc (* h 0.25)))) + (trick-card 3 (+ cx (round (* w 0.4))) (round (- cyc (* h 0.25))))) + (txt (format "%s%s" (aref cg-bridge-seat-names act) + (cond ((eq phase 'auction) " (you)") + ((= act 0) " (you)") + (t " (dummy -- you play)"))) + pad (- y-hand 6) 11) + (let ((x (max pad (- (/ width 2) (/ fanw 2)))) (i 0) + (legalp (and (eq phase 'play) (= turn act)))) + (dolist (c ahand) + (let ((sp (cg-bridge--spec c)) (curp (= i cursor)) + (hintp (and legalp (cg-bridge--legal-play-p game act c)))) + (cg-svg-card svg x y-hand :rank (car sp) :suit (cdr sp) + :highlight curp :hint hintp) + (push (cons (list x y-hand (if (= i (1- n)) w step) h) (cons 'hand i)) regions)) + (setq x (+ x step) i (1+ i)))) + (txt (or (cg-get game :message) "") pad (- height 8) 12)) + (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions (nreverse regions)))) + (cl-defmethod cg-render ((game cg-bridge-game)) - "Return a propertized depiction of the Bridge GAME." + "Return a depiction of the Bridge GAME: SVG board if graphical, else text." + (if (and cg-bridge-svg-cards (display-graphic-p)) + (cg-bridge--svg game) + (cg-bridge--render-text game))) + +(defun cg-bridge--render-text (game) + "Return a plain-text depiction of the Bridge GAME." (let* ((out '()) (phase (cg-get game :phase)) (cursor (cg-get game :cursor))) (push " Bridge\n" out) (push (format " Rubber: You/North games %d East/West games %d%s\n" diff --git a/cg-core.el b/cg-core.el index a831fe2..7dafdfe 100644 --- a/cg-core.el +++ b/cg-core.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -73,6 +73,17 @@ and SPC as an action key. Takes effect the next time a game starts." (const :tag "Classic (adds hjkl, SPC)" classic)) :group 'card-games) +(defcustom cg-ai-level 'normal + "Difficulty of the computer opponents, where a game supports it. +`easy' plays a quick, simple game, `normal' plays soundly, and `hard' +thinks a little harder. Honoured by Russian Bank (Crapette) and the +trick-taking games so far; other games ignore it for now. Change it from +the `card-game' menu or with `card-games-set-ai-level'." + :type '(choice (const :tag "Easy" easy) + (const :tag "Normal" normal) + (const :tag "Hard" hard)) + :group 'card-games) + (defclass cg-game () ((name :initarg :name :initform "game" :type string :documentation "Human-readable game name.") @@ -196,8 +207,10 @@ The glyphs are taken from `cg-symbols'." "?")) (defsubst cg-red-suit-p (suit) - "Return non-nil when SUIT index denotes a red suit." - (memq suit '(2 3))) + "Return t when SUIT index denotes a red suit, else nil. +Normalised to a boolean so callers may compare two results with `eq' +\(diamonds and hearts are both red but `memq' returns different tails)." + (and (memq suit '(2 3)) t)) (defsubst cg-sister-suit (suit) "Return the other suit index of the same colour as SUIT." diff --git a/cg-crapette.el b/cg-crapette.el index 1faaeac..d9f302b 100644 --- a/cg-crapette.el +++ b/cg-crapette.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -56,9 +56,11 @@ ;; somewhere you keep going, otherwise it goes to your waste and your turn ;; ends. ;; -;; SIMPLIFICATION (documented): the AI is a straightforward greedy player -;; and always observes foundation priority, so in practice only you can be -;; "stopped". +;; The AI observes foundation priority, empties its reserve first (the +;; bottleneck), prefers loading its cards onto you, and looks one move +;; ahead to rearrange the houses when that frees a stuck reserve or waste +;; card. It never breaks foundation priority, so in practice only you can +;; be "stopped". ;;; Code: @@ -433,11 +435,56 @@ Return non-nil when the offending action must be abandoned by its caller." for fi = (and card (cg-crap--found-for game card)) when fi return (cons spot (cons 'found fi)))) -(defun cg-crap--ai-useful-move (game) - "Return an AI (SOURCE . DEST) move that empties its reserve or waste, or nil. -The AI only moves its own reserve/waste tops -- onto your piles (loading) -or onto a house -- so every such move reduces the AI's cards and its turn -is guaranteed to end." +(defun cg-crap--ai-unload-move (game) + "Return the best AI (SOURCE . DEST) move that empties its reserve or waste. +Emptying the RESERVE is the goal of the game, so it outscores the waste; +LOADING a card onto you (which also burdens you) outscores building a +house. Every such move reduces the AI's own cards, so its turn ends." + (let ((best nil) (bestscore 0)) + (dolist (spot (list (cons 'res 1) (cons 'was 1))) + (let ((card (cg-crap--spot-top game spot)) + (base (if (eq (car spot) 'res) 40 0))) ; the reserve is the bottleneck + (when card + (dolist (dst (list (cons 'res 0) (cons 'was 0))) + (when (cg-crap--load-accepts (cg-crap--spot-top game dst) card) + (let ((sc (+ base 60))) ; loading: rid a card AND burden you + (when (> sc bestscore) + (setq bestscore sc best (cons spot dst)))))) + (cl-loop for i below 8 + when (cg-crap--house-accepts game i card) + do (let ((sc (+ base 50))) ; else build it onto a house + (when (> sc bestscore) + (setq bestscore sc best (cons spot (cons 'house i))))) + (cl-return))))) + best)) + +(defun cg-crap--ai-enabling-move (game) + "Return a single-card house->house move that unlocks an unload, or nil. +This is the crafty bit: when the AI cannot place its reserve or waste top +anywhere, it looks one move ahead for a house rearrangement that would +make such a placement legal. It only fires when no direct unload exists, +and only when the shuffle genuinely opens one, so the turn still ends." + (when (null (cg-crap--ai-unload-move game)) + (catch 'found + (dotimes (i 8) + (dotimes (j 8) + (let ((pilei (cg-crap--house game i)) (pilej (cg-crap--house game j))) + (when (and (/= i j) pilei) + (let ((card (cg-crap--top pilei))) + (when (cg-crap--house-accepts game j card) + (aset (cg-get game :houses) i (butlast pilei 1)) + (aset (cg-get game :houses) j (append pilej (list card))) + (let ((opens (cg-crap--ai-unload-move game))) + (aset (cg-get game :houses) i pilei) + (aset (cg-get game :houses) j pilej) + (when opens + (throw 'found (cons (cons 'house i) (cons 'house j))))))))))) + nil))) + +(defun cg-crap--ai-greedy-move (game) + "A simple first-fit unload move -- the `easy' AI. +Empties the reserve or waste top onto the first legal spot, without the +scoring or the house-rearranging lookahead of the tougher levels." (catch 'm (dolist (spot (list (cons 'res 1) (cons 'was 1))) (let ((card (cg-crap--spot-top game spot))) @@ -451,14 +498,17 @@ is guaranteed to end." nil)) (defun cg-crap--ai-play (game) - "Play the AI opponent's whole turn on GAME." - (let ((guard 0)) + "Play the AI opponent's whole turn on GAME, per `cg-ai-level'." + (let ((guard 0) (level cg-ai-level)) (catch 'done (while t - (when (> (setq guard (1+ guard)) 400) (throw 'done nil)) + (when (> (setq guard (1+ guard)) 800) (throw 'done nil)) (when (cg-crap--won-p game 1) (throw 'done nil)) (let ((mv (or (cg-crap--ai-found-move game) - (cg-crap--ai-useful-move game)))) + (if (eq level 'easy) + (cg-crap--ai-greedy-move game) + (cg-crap--ai-unload-move game)) + (and (eq level 'hard) (cg-crap--ai-enabling-move game))))) (cond (mv (cg-crap--move game (car mv) (cdr mv) 1)) ((cg-crap--hand game 1) diff --git a/cg-cribbage.el b/cg-cribbage.el index ecacb3c..25e24dd 100644 --- a/cg-cribbage.el +++ b/cg-cribbage.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -297,8 +297,83 @@ TOTAL is the running count after the play." (defvar-local cg-crib--game nil "The Cribbage game in the current buffer.") +(defun cg-crib--svg (game) + "Return an SVG board for the Cribbage GAME (with a peg-track)." + (let* ((w cg-svg-card-width) (h cg-svg-card-height) (gap cg-svg-card-gap) (pad 16) + (phase (cg-get game :phase)) (scores (cg-get game :scores)) + (hand (if (eq phase 'play) (cg-crib--play game 0) (cg-crib--hand game 0))) + (n (length hand)) (cursor (cg-get game :cursor)) (marks (cg-get game :marks)) + (overlap (cond ((> n 11) (- w 24)) ((> n 8) 20) (t 0))) + (step (max 14 (- (+ w gap) overlap))) + (fanw (if (> n 0) (+ (* (1- n) step) w) w)) + (target cg-cribbage-target) (barw 220) (peg-h 14) (peg-gap 8) + (y-title 6) (y-peg 26) + (y-mid (+ y-peg (* 2 (+ peg-h peg-gap)) 18)) + (y-hand (+ y-mid h 44)) + (height (+ y-hand h 30)) + (width (max (+ fanw (* 2 pad)) (+ pad 90 barw 120) 620)) + (svg (svg-create width height)) + (lc (cg-color 'shadow :foreground "gray50")) + (regions '())) + (cl-labels ((txt (str x y &optional sz bold) + (apply #'svg-text svg str :x x :y y :font-size (or sz 12) :fill lc + :font-family cg-svg-font-family (and bold '(:font-weight "bold")))) + (peg (label sc y) + (txt label pad (+ y 11) 12) + (let ((bx (+ pad 90))) + (svg-rectangle svg bx y barw peg-h :rx 4 :fill "none" + :stroke lc :stroke-width 1) + (svg-rectangle svg bx y + (round (* barw (/ (float (min sc target)) target))) + peg-h :rx 4 :fill "#3aa15a") + (txt (format "%d" sc) (+ bx barw 8) (+ y 11) 12))) + (crow (cards x y) + (let ((xx x)) + (dolist (c cards) + (let ((sp (cg-rummy--card-spec c))) + (cg-svg-card svg xx y :rank (car sp) :suit (cdr sp))) + (setq xx (+ xx (round (* w 0.5)))))))) + (txt (format "Cribbage (to %d)" target) pad (+ y-title 12) 13 t) + (peg "You" (aref scores 0) y-peg) + (peg "Computer" (aref scores 1) (+ y-peg peg-h peg-gap)) + (txt (format "%s deals" (cg-crib--who (cg-get game :dealer))) + (+ pad 90 barw 60) (+ y-peg 11) 11) + (let ((mx pad)) + (when (cg-get game :starter) + (let ((sp (cg-rummy--card-spec (cg-get game :starter)))) + (cg-svg-card svg mx y-mid :rank (car sp) :suit (cdr sp)) + (txt "Starter" mx (+ y-mid h 13) 11) + (setq mx (+ mx w gap 24)))) + (cond + ((eq phase 'play) + (txt (format "Count: %d" (cg-get game :total)) mx (- y-mid 4) 12) + (crow (reverse (cg-get game :seq)) mx y-mid)) + ((memq phase '(show game-over)) + (when (cg-get game :crib) + (txt (format "Crib (%s)" (cg-crib--who (cg-get game :dealer))) mx (- y-mid 4) 11) + (crow (cg-get game :crib) mx y-mid))))) + (txt (format "Your %s" (if (eq phase 'play) "cards" "hand")) pad (- y-hand 6) 11) + (let ((x (max pad (- (/ width 2) (/ fanw 2)))) (i 0)) + (dolist (c hand) + (let ((sp (cg-rummy--card-spec c)) (curp (= i cursor)) + (markp (and marks (memq i marks)))) + (cg-svg-card svg x y-hand :rank (car sp) :suit (cdr sp) :highlight curp) + (when markp + (svg-rectangle svg (- x 3) (- y-hand 3) (+ w 6) (+ h 6) + :rx 8 :fill "none" :stroke "#4a90d9" :stroke-width 3)) + (push (cons (list x y-hand (if (= i (1- n)) w step) h) (cons 'hand i)) regions)) + (setq x (+ x step) i (1+ i)))) + (txt (or (cg-get game :message) "") pad (- height 8) 12)) + (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions (nreverse regions)))) + (cl-defmethod cg-render ((game cg-cribbage-game)) - "Return a propertized depiction of the Cribbage GAME." + "Return a depiction of the Cribbage GAME: SVG board if graphical, else text." + (if (and cg-rummy-svg-cards (display-graphic-p)) + (cg-crib--svg game) + (cg-crib--render-text game))) + +(defun cg-crib--render-text (game) + "Return a plain-text depiction of the Cribbage GAME." (let* ((out '()) (scores (cg-get game :scores)) (phase (cg-get game :phase)) (cursor (cg-get game :cursor))) (push (format " Cribbage to %d\n\n" cg-cribbage-target) out) diff --git a/cg-eights.el b/cg-eights.el index b78a9d8..238c752 100644 --- a/cg-eights.el +++ b/cg-eights.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -213,8 +213,65 @@ Return the drawn card, or nil when none is available." "Return the cg-svg display spec (RANK-STRING . SUIT) for CARD." (cons (aref cg-eights-ranks (cdr card)) (car card))) +(defun cg-eights--board-svg (game) + "Return an SVG board for the Crazy Eights GAME." + (let* ((w cg-svg-card-width) (h cg-svg-card-height) (gap cg-svg-card-gap) (pad 16) + (hand (cg-eights--hand game 0)) (n (length hand)) + (cursor (cg-get game :cursor)) + (top (cg-eights--top game)) (suit (cg-get game :suit)) + (overlap (cond ((> n 11) (- w 24)) ((> n 8) 20) (t 0))) + (step (max 14 (- (+ w gap) overlap))) + (fanw (if (> n 0) (+ (* (1- n) step) w) w)) + (np (cg-get game :nplayers)) (nstock (length (cg-get game :stock))) + (y-title 6) (y-info 26) + (y-mid (+ y-info (* (1- np) 16) 14)) + (y-hand (+ y-mid h 42)) + (height (+ y-hand h 30)) + (width (max (+ fanw (* 2 pad)) 560)) + (svg (svg-create width height)) + (lc (cg-color 'shadow :foreground "gray50")) + (regions '())) + (cl-labels ((txt (str x y &optional sz bold) + (apply #'svg-text svg str :x x :y y :font-size (or sz 12) :fill lc + :font-family cg-svg-font-family (and bold '(:font-weight "bold"))))) + (txt "Crazy Eights" pad (+ y-title 12) 13 t) + (let ((yy (+ y-info 4))) + (dotimes (s np) + (unless (= s 0) + (txt (format "Player %d: %d cards (score %d)" s + (length (cg-eights--hand game s)) (aref (cg-get game :scores) s)) + pad yy 12) + (setq yy (+ yy 16))))) + (cg-svg-card svg pad y-mid :down (> nstock 0) :gap (= nstock 0)) + (txt (format "Stock %d" nstock) pad (+ y-mid h 13) 11) + (let ((dx (+ pad w gap 28)) (sp (cg-eights--spec top))) + (cg-svg-card svg dx y-mid :rank (car sp) :suit (cdr sp)) + (txt "Discard" dx (+ y-mid h 13) 11) + (let ((sx (+ dx w gap 34)) + (col (if (cg-red-suit-p suit) "#c0392b" "#2c3e50"))) + (txt "Suit in play" sx (- y-mid 4) 11) + (svg-text svg (cg-suit-glyph suit) :x (+ sx 12) :y (+ y-mid 46) + :font-size 44 :fill col :font-family cg-svg-font-family))) + (txt "Your hand" pad (- y-hand 6) 11) + (let ((x (max pad (- (/ width 2) (/ fanw 2)))) (i 0)) + (dolist (c hand) + (let ((sp (cg-eights--spec c)) (curp (= i cursor)) + (hintp (cg-eights--legal-p game c))) + (cg-svg-card svg x y-hand :rank (car sp) :suit (cdr sp) + :highlight curp :hint hintp) + (push (cons (list x y-hand (if (= i (1- n)) w step) h) (cons 'hand i)) regions)) + (setq x (+ x step) i (1+ i)))) + (txt (or (cg-get game :message) "") pad (- height 8) 12)) + (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions (nreverse regions)))) + (cl-defmethod cg-render ((game cg-eights-game)) - "Return a propertized string depicting GAME for a text display." + "Return a depiction of GAME: an SVG board if graphical, else text." + (if (and cg-eights-svg-cards (display-graphic-p)) + (cg-eights--board-svg game) + (cg-eights--render-text game))) + +(defun cg-eights--render-text (game) + "Return a plain-text depiction of GAME." (let* ((out (list)) (top (cg-eights--top game)) (hand (cg-eights--hand game 0)) (cursor (cg-get game :cursor))) (push (format " Crazy Eights\n\n") out) diff --git a/cg-gaps.el b/cg-gaps.el index f60feea..8a53833 100644 --- a/cg-gaps.el +++ b/cg-gaps.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-handfoot.el b/cg-handfoot.el index 07acc06..352e9e8 100644 --- a/cg-handfoot.el +++ b/cg-handfoot.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-match.el b/cg-match.el index 3e67917..951d25f 100644 --- a/cg-match.el +++ b/cg-match.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -452,8 +452,69 @@ instead of hunting for one overlapped card in a big hand." (defvar-local cg-om--game nil "The Old Maid game in the current buffer.") +(defun cg-om--svg (game) + "Return an SVG board for the Old Maid GAME." + (let* ((w cg-svg-card-width) (h cg-svg-card-height) (gap cg-svg-card-gap) (pad 16) + (hand (cg-om--hand game 0)) (n (length hand)) + (target (cg-om--target game 0)) (pick (or (cg-get game :pick) 0)) + (yourp (and target (eq (cg-get game :phase) 'play) (= (cg-get game :turn) 0))) + (np (cg-get game :nplayers)) + (overlap (cond ((> n 11) (- w 24)) ((> n 8) 20) (t 0))) + (step (max 14 (- (+ w gap) overlap))) + (fanw (if (> n 0) (+ (* (1- n) step) w) w)) + (bstep 20) + (tn (and target (length (cg-om--hand game target)))) + (y-title 6) (y-info 26) + (y-target (+ y-info (* (1- np) 16) 18)) + (y-hand (+ y-target h 42)) + (targetw (if (and yourp tn (> tn 0)) (+ (* (1- tn) bstep) w) 0)) + (height (+ y-hand h 30)) + (width (max (+ fanw (* 2 pad)) (+ targetw (* 2 pad)) 560)) + (svg (svg-create width height)) + (lc (cg-color 'shadow :foreground "gray50")) + (regions '())) + (cl-labels ((txt (str x y &optional sz bold) + (apply #'svg-text svg str :x x :y y :font-size (or sz 12) :fill lc + :font-family cg-svg-font-family (and bold '(:font-weight "bold"))))) + (txt "Old Maid" pad (+ y-title 12) 13 t) + (let ((yy (+ y-info 4))) + (dotimes (s np) + (unless (= s 0) + (txt (format "Player %d: %d cards%s" s (length (cg-om--hand game s)) + (if (eql s target) " <- draw from here" "")) + pad yy 12) + (setq yy (+ yy 16))))) + (when (and yourp tn (> tn 0)) + (txt (format "Pick a card from Player %d:" target) pad (- y-target 6) 11) + (let ((x pad)) + (dotimes (i tn) + (cg-svg-card svg x y-target :down t :highlight (= i pick)) + (push (cons (list x y-target (if (= i (1- tn)) w bstep) h) (cons 'pick i)) + regions) + (setq x (+ x bstep))))) + (txt "Your hand" pad (- y-hand 6) 11) + (let ((x (max pad (- (/ width 2) (/ fanw 2))))) + (dolist (c hand) + (let ((sp (cg-rummy--card-spec c))) + (cg-svg-card svg x y-hand :rank (car sp) :suit (cdr sp))) + (setq x (+ x step)))) + (txt (or (cg-get game :message) "") pad (- height 8) 12)) + (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions (nreverse regions)))) + +(cl-defmethod cg-render-apply ((g cg-old-maid-game) action) + "Apply a click ACTION: pick that card from the target and draw it." + (pcase action + (`(pick . ,i) (cg-put g :pick i) (cg-om-draw)) + (_ (cl-call-next-method)))) + (cl-defmethod cg-render ((game cg-old-maid-game)) - "Return a propertized depiction of the Old Maid GAME." + "Return a depiction of the Old Maid GAME: SVG board if graphical, else text." + (if (and cg-rummy-svg-cards (display-graphic-p)) + (cg-om--svg game) + (cg-om--render-text game))) + +(defun cg-om--render-text (game) + "Return a plain-text depiction of the Old Maid GAME." (let* ((out '()) (target (cg-om--target game 0))) (push " Old Maid\n\n" out) (dotimes (s (cg-get game :nplayers)) @@ -472,6 +533,7 @@ instead of hunting for one overlapped card in a big hand." (defun cg-om--redisplay () (let ((game cg-om--game) (inhibit-read-only t)) + (setq cg-current-game game cg-redisplay-function #'cg-om--redisplay) (setq-local mode-line-process (format " [%s]" (cg-get game :phase))) (erase-buffer) (insert (cg-render game)) (goto-char (point-min)))) @@ -510,6 +572,11 @@ instead of hunting for one overlapped card in a big hand." (defvar cg-old-maid-mode-map (let ((map (make-sparse-keymap))) + (define-key map [mouse-1] #'cg-card-click) + (define-key map "+" #'cg-card-zoom-in) + (define-key map "=" #'cg-card-zoom-in) + (define-key map "-" #'cg-card-zoom-out) + (define-key map "0" #'cg-card-zoom-reset) (define-key map (kbd "") #'cg-om-left) (define-key map (kbd "") #'cg-om-right) (define-key map (kbd "RET") #'cg-om-draw) diff --git a/cg-net.el b/cg-net.el index cff0ced..8019c5e 100644 --- a/cg-net.el +++ b/cg-net.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-patience.el b/cg-patience.el index e3bb1e9..6deebc0 100644 --- a/cg-patience.el +++ b/cg-patience.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-president.el b/cg-president.el index 80bc694..11ada49 100644 --- a/cg-president.el +++ b/cg-president.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-render.el b/cg-render.el index 265a619..d031d93 100644 --- a/cg-render.el +++ b/cg-render.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-rum500.el b/cg-rum500.el index d5b9ada..9c92630 100644 --- a/cg-rum500.el +++ b/cg-rum500.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-rummy.el b/cg-rummy.el index c41450f..98c42bf 100644 --- a/cg-rummy.el +++ b/cg-rummy.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-scopa.el b/cg-scopa.el index 17d8a7c..6947920 100644 --- a/cg-scopa.el +++ b/cg-scopa.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-solitaire.el b/cg-solitaire.el index 2ead28f..c186c2e 100644 --- a/cg-solitaire.el +++ b/cg-solitaire.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-spite.el b/cg-spite.el index 2ff7ee2..83a30bf 100644 --- a/cg-spite.el +++ b/cg-spite.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -273,8 +273,74 @@ (push (format "%d:%s" (1+ d) (if top (cg-rummy-card-string top) "--")) parts))) (mapconcat #'identity (nreverse parts) " "))) +(defun cg-spite--board-svg (game) + "Return an SVG board for the Spite & Malice GAME." + (let* ((w cg-svg-card-width) (h cg-svg-card-height) (gap cg-svg-card-gap) (pad 16) + (hand (cg-spite--hand game 0)) (n (length hand)) + (cursor (cg-get game :cursor)) (center (cg-get game :center)) + (overlap (cond ((> n 11) (- w 24)) ((> n 8) 20) (t 0))) + (step (max 14 (- (+ w gap) overlap))) + (fanw (if (> n 0) (+ (* (1- n) step) w) w)) + (colstep (+ w 14)) + (y-title 6) (y-opp 26) + (y-center (+ y-opp 16)) + (y-sm (+ y-center h 16)) + (y-ylabel (+ y-sm 12)) + (y-yours (+ y-ylabel 6)) + (y-hand (+ y-yours h 42)) + (height (+ y-hand h 30)) + (width (max (+ fanw (* 2 pad)) (+ (* 5 colstep) (* 2 pad)) 620)) + (svg (svg-create width height)) + (lc (cg-color 'shadow :foreground "gray50")) + (regions '())) + (cl-labels ((txt (str x y &optional sz bold) + (apply #'svg-text svg str :x x :y y :font-size (or sz 12) :fill lc + :font-family cg-svg-font-family (and bold '(:font-weight "bold")))) + (pilecard (spec x y) + (if spec (cg-svg-card svg x y :rank (car spec) :suit (cdr spec)) + (cg-svg-card svg x y :gap t)))) + (txt (format "Spite & Malice (goal %d)" cg-spite-goal-size) pad (+ y-title 12) 13 t) + (txt (format "Computer: goal %d left hand %d discards %s" + (length (cg-spite--goal game 1)) (length (cg-spite--hand game 1)) + (cg-spite--disc-string game 1)) + pad (+ y-opp 4) 12) + (txt "Centre (build A..Q; King is wild)" pad (- y-center 4) 11) + (dotimes (i 4) + (let* ((x (+ pad (* i colstep))) (pp (aref center i)) + (spec (and pp (cons (aref cg-rummy-ranks (car pp)) (car (cadr pp)))))) + (pilecard spec x y-center))) + (txt (format "Stock %d Muck %d" + (length (cg-get game :stock)) (length (cg-get game :muck))) + pad y-sm 11) + (let* ((gtop (car (cg-spite--goal game 0))) + (gspec (and gtop (cg-rummy--card-spec gtop)))) + (txt (format "Your goal (%d left)" (length (cg-spite--goal game 0))) + pad y-ylabel 11) + (txt "Discards" (+ pad colstep) y-ylabel 11) + (pilecard gspec pad y-yours) + (dotimes (d 4) + (let* ((x (+ pad colstep (* d colstep))) + (dtop (car (aref (cg-spite--disc game 0) d))) + (dspec (and dtop (cg-rummy--card-spec dtop)))) + (pilecard dspec x y-yours)))) + (txt "Your hand" pad (- y-hand 6) 11) + (let ((x (max pad (- (/ width 2) (/ fanw 2)))) (i 0)) + (dolist (c hand) + (let ((sp (cg-rummy--card-spec c)) (curp (= i cursor))) + (cg-svg-card svg x y-hand :rank (car sp) :suit (cdr sp) :highlight curp) + (push (cons (list x y-hand (if (= i (1- n)) w step) h) (cons 'hand i)) regions)) + (setq x (+ x step) i (1+ i)))) + (txt (or (cg-get game :message) "") pad (- height 8) 12)) + (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions (nreverse regions)))) + (cl-defmethod cg-render ((game cg-spite-game)) - "Return a propertized depiction of the Spite & Malice GAME." + "Return a depiction of the GAME: an SVG board if graphical, else text." + (if (and cg-rummy-svg-cards (display-graphic-p)) + (cg-spite--board-svg game) + (cg-spite--render-text game))) + +(defun cg-spite--render-text (game) + "Return a plain-text depiction of the Spite & Malice GAME." (let* ((out '()) (cursor (cg-get game :cursor))) (push " Spite & Malice\n\n" out) (push (format " Computer goal: %d left hand: %d discards: %s\n\n" diff --git a/cg-svg.el b/cg-svg.el index 5141276..d549af0 100644 --- a/cg-svg.el +++ b/cg-svg.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -86,9 +86,47 @@ not themed this way -- see `cg-svg--highlight' -- so it never picks up a theme's `region' colour." :type 'boolean :group 'cg-svg) -(defcustom cg-svg-card-back 'dots - "Pattern drawn on a face-down card back." - :type '(choice (const dots) (const rings) (const solid)) :group 'cg-svg) +(defcustom cg-svg-card-back 'random + "Pattern drawn on a face-down card back. +The `emacs', `emacs-classic', `gnu' and `splash' backs stamp the card with +a logo that ships with Emacs. `random' picks one of the concrete backs +for the session (reshuffle with `cg-svg-shuffle-card-back')." + :type '(choice (const dots) (const rings) (const solid) + (const lattice) (const waves) (const diamond) + (const emacs) (const emacs-classic) (const gnu) (const splash) + (const random)) + :group 'cg-svg) + +(defconst cg-svg--card-backs + '(dots rings solid lattice waves diamond emacs emacs-classic gnu splash) + "Concrete card backs that `random' chooses among.") + +(defvar cg-svg--random-back nil + "The concrete back currently chosen for the `random' setting.") + +(defun cg-svg--roll-back () + "Choose a fresh concrete back for `random' and return it." + (setq cg-svg--random-back + (nth (random (length cg-svg--card-backs)) cg-svg--card-backs))) + +;;;###autoload +(defun cg-svg-shuffle-card-back () + "Pick a new random card back (used when `cg-svg-card-back' is `random')." + (interactive) + (cg-svg--roll-back) + (when (called-interactively-p 'interactive) + (message "Card back: %s" cg-svg--random-back))) + +(defun cg-svg--effective-back () + "Return the concrete back to draw, resolving `random'." + (if (eq cg-svg-card-back 'random) + (or cg-svg--random-back (cg-svg--roll-back)) + cg-svg-card-back)) + +(defun cg-svg--back-logo-name (back) + "Map a logo card-back BACK to a `cg-svg--logo-files' key." + (pcase back ('emacs 'modern) ('emacs-classic 'classic) + ('gnu 'gnu) ('splash 'splash))) (defcustom cg-svg-four-color nil "Use a four-colour deck when non-nil. @@ -255,6 +293,60 @@ X, Y and W, H give the card's top-left corner and size." (cg-svg--text svg "★" (+ x (/ w 2.0)) (+ y (* h 0.52)) (* h 0.40) color) (cg-svg--text svg "JOKER" (+ x (/ w 2.0)) (+ y (* h 0.74)) (* h 0.135) color t)) +(defun cg-svg--back-dots (svg x y w h) + "Draw the dotted-medallion back pattern." + (let ((gy (+ y 10))) + (while (< gy (- (+ y h) 8)) + (let ((gx (+ x 10))) + (while (< gx (- (+ x w) 8)) + (svg-circle svg gx gy 1.1 :fill cg-svg-back-trim) + (setq gx (+ gx 9)))) + (setq gy (+ gy 9))))) + +(defun cg-svg--back-lattice (svg x y w h) + "Draw a small-cross lattice back pattern." + (let ((gy (+ y 13))) + (while (< gy (- (+ y h) 10)) + (let ((gx (+ x 13))) + (while (< gx (- (+ x w) 10)) + (svg-line svg (- gx 2) (- gy 2) (+ gx 2) (+ gy 2) + :stroke cg-svg-back-trim :stroke-width 1) + (svg-line svg (- gx 2) (+ gy 2) (+ gx 2) (- gy 2) + :stroke cg-svg-back-trim :stroke-width 1) + (setq gx (+ gx 11)))) + (setq gy (+ gy 11))))) + +(defun cg-svg--back-waves (svg x y w h) + "Draw a staggered-dash (brickwork) back pattern." + (let ((gy (+ y 12)) (row 0)) + (while (< gy (- (+ y h) 9)) + (let ((gx (+ x (if (cl-evenp row) 9 15)))) + (while (< gx (- (+ x w) 9)) + (svg-line svg gx gy (+ gx 6) gy :stroke cg-svg-back-trim :stroke-width 1.4) + (setq gx (+ gx 12)))) + (setq gy (+ gy 8) row (1+ row))))) + +(defun cg-svg--back-diamond (svg x y w h) + "Draw concentric diamonds as the back pattern." + (let ((cx (+ x (/ w 2.0))) (cy (+ y (/ h 2.0)))) + (dolist (f '(0.40 0.28 0.16)) + (let ((dw (* w f)) (dh (* h f))) + (svg-polygon svg (list (cons cx (- cy dh)) (cons (+ cx dw) cy) + (cons cx (+ cy dh)) (cons (- cx dw) cy)) + :fill "none" :stroke cg-svg-back-trim :stroke-width 1))))) + +(defun cg-svg--back-logo (svg x y w h back) + "Stamp the Emacs logo for BACK centred on the card, or dots if unavailable." + (let ((file (and (fboundp 'svg-embed) + (cg-svg--logo-file (cg-svg--back-logo-name back))))) + (if (null file) + (cg-svg--back-dots svg x y w h) + (let ((size (round (* h 0.52)))) + (svg-embed svg file "image/png" nil + :x (round (+ x (/ (- w size) 2.0))) + :y (round (+ y (/ (- h size) 2.0))) + :width size :height size))))) + (defun cg-svg--draw-back (svg x y w h r) "Draw a face-down card back on SVG at X, Y (W by H, corner R). The pattern is controlled by `cg-svg-card-back'." @@ -262,21 +354,20 @@ The pattern is controlled by `cg-svg-card-back'." :stroke cg-svg-border-color :stroke-width 1) (svg-rectangle svg (+ x 4) (+ y 4) (- w 8) (- h 8) :rx 4 :fill "none" :stroke cg-svg-back-trim :stroke-width 1) - (pcase cg-svg-card-back - ('solid nil) - ('rings - (svg-rectangle svg (+ x 8) (+ y 8) (- w 16) (- h 16) :rx 5 :fill "none" - :stroke cg-svg-back-trim :stroke-width 1) - (svg-rectangle svg (+ x 12) (+ y 12) (- w 24) (- h 24) :rx 4 :fill "none" - :stroke cg-svg-back-trim :stroke-width 1)) - (_ - (let ((gy (+ y 10))) - (while (< gy (- (+ y h) 8)) - (let ((gx (+ x 10))) - (while (< gx (- (+ x w) 8)) - (svg-circle svg gx gy 1.1 :fill cg-svg-back-trim) - (setq gx (+ gx 9)))) - (setq gy (+ gy 9))))))) + (let ((back (cg-svg--effective-back))) + (pcase back + ('solid nil) + ('rings + (svg-rectangle svg (+ x 8) (+ y 8) (- w 16) (- h 16) :rx 5 :fill "none" + :stroke cg-svg-back-trim :stroke-width 1) + (svg-rectangle svg (+ x 12) (+ y 12) (- w 24) (- h 24) :rx 4 :fill "none" + :stroke cg-svg-back-trim :stroke-width 1)) + ('lattice (cg-svg--back-lattice svg x y w h)) + ('waves (cg-svg--back-waves svg x y w h)) + ('diamond (cg-svg--back-diamond svg x y w h)) + ((or 'emacs 'emacs-classic 'gnu 'splash) + (cg-svg--back-logo svg x y w h back)) + (_ (cg-svg--back-dots svg x y w h))))) (defun cg-svg--draw-face (svg x y w h r rank suit) "Draw a face-up card (RANK of SUIT) on SVG at X, Y (W by H, corner R)." @@ -471,5 +562,59 @@ card-size slider beneath the row." (cg-svg-slider-draw svg pad (+ pad h 8) cg-card-scale))) (propertize "*" 'display (cg-svg-image svg (cg-scale)) 'cg-regions regions)))) +(defcustom cg-svg-emacs-logo 'modern + "Which Emacs emblem to show in the full-window (svg-fill) games. +The image choices embed a logo that ships with Emacs, falling back to the +drawn emblem when the file is unavailable. `drawn' is a small built-in +emblem and `none' shows nothing." + :type '(choice (const :tag "Modern Emacs icon" modern) + (const :tag "Classic Emacs icon" classic) + (const :tag "GNU head (Gnus)" gnu) + (const :tag "GNU Emacs splash" splash) + (const :tag "Drawn emblem" drawn) + (const :tag "None" none)) + :group 'cg-svg) + +(defconst cg-svg--logo-files + '((modern . ("images/icons/hicolor/48x48/apps/emacs.png" + "images/icons/hicolor/128x128/apps/emacs.png")) + (classic . ("images/icons/hicolor/48x48/apps/emacs23.png" + "images/icons/hicolor/128x128/apps/emacs23.png")) + (gnu . ("images/gnus/gnus.png")) + (splash . ("images/splash.png"))) + "Map a logo name to candidate image files relative to `data-directory'.") + +(defun cg-svg--logo-file (name) + "Return the first readable image file for logo NAME, or nil." + (cl-loop for rel in (cdr (assq name cg-svg--logo-files)) + for f = (expand-file-name rel data-directory) + when (file-readable-p f) return f)) + +(defun cg-svg--draw-logo-emblem (svg cx cy fs) + "Draw the built-in purple GNU Emacs emblem centred at CX, CY, scaled FS." + (svg-gradient svg "cg-logo" 'linear '((0 . "#8056c8") (100 . "#3f1f9e"))) + (svg-circle svg cx cy (round (* 26 fs)) :gradient "cg-logo" + :stroke "#2a1370" :stroke-width 2) + (cg-svg--text svg "e" cx (+ cy (round (* 10 fs))) (round (* 30 fs)) "#ffffff" t) + (cg-svg--text svg "GNU Emacs" cx (+ cy (round (* 42 fs))) + (max 10 (round (* 11 fs))) "#c7bbe6")) + +(defun cg-svg-draw-logo (svg cx cy &optional fs) + "Draw the configured Emacs emblem (`cg-svg-emacs-logo') centred at CX, CY. +FS scales it. Embeds a real Emacs logo image when one is available, and +otherwise draws the built-in emblem." + (let ((fs (or fs 1.0)) (choice cg-svg-emacs-logo)) + (pcase choice + ('none nil) + ('drawn (cg-svg--draw-logo-emblem svg cx cy fs)) + (_ (let ((file (and (fboundp 'svg-embed) (cg-svg--logo-file choice)))) + (if (null file) + (cg-svg--draw-logo-emblem svg cx cy fs) + (let ((size (round (* 56 fs)))) + (svg-embed svg file "image/png" nil + :x (round (- cx (/ size 2))) + :y (round (- cy (/ size 2))) + :width size :height size)))))))) + (provide 'cg-svg) ;;; cg-svg.el ends here diff --git a/cg-trick-ext.el b/cg-trick-ext.el index a5b9050..57409da 100644 --- a/cg-trick-ext.el +++ b/cg-trick-ext.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el diff --git a/cg-trick.el b/cg-trick.el index dae410b..0d5cd83 100644 --- a/cg-trick.el +++ b/cg-trick.el @@ -4,7 +4,7 @@ ;; Author: Corwin Brust ;; Maintainer: Corwin Brust -;; Version: 1.0.90 +;; Version: 1.0.91 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el @@ -394,7 +394,11 @@ "Play a whole hand with AI for every seat (used by tests)." (while (not (cg-trick--hand-over-p game)) (let ((seat (cg-get game :turn))) - (cg-trick--play game seat (cg-trick--ai-play game seat)))) + (cg-trick--play game seat + (if (eq cg-ai-level 'easy) + (let ((moves (cg-trick--legal-moves game seat))) + (nth (random (length moves)) moves)) + (cg-trick--ai-play game seat))))) (cg-trick--score-hand game)) ;;;; New-game / hand lifecycle diff --git a/doc/card-games.texi b/doc/card-games.texi new file mode 100644 index 0000000..17af0ed --- /dev/null +++ b/doc/card-games.texi @@ -0,0 +1,1182 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename card-games.info +@settitle Card Games for Emacs +@documentencoding UTF-8 +@documentlanguage en +@syncodeindex vr cp +@syncodeindex fn cp +@c %**end of header + +@include version.texi + +@copying +This manual is for Card Games for Emacs (version @value{VERSION}), an +Emacs package that plays card games as UNICODE text in a terminal and as +SVG cards on a graphical display. + +Copyright @copyright{} @value{YEAR} Corwin Brust. + +@quotation +This manual is licensed under the Creative Commons Attribution 4.0 +International License. To view a copy of this license, visit +@url{https://creativecommons.org/licenses/by/4.0/}. + +The Card Games for Emacs @emph{program} is free software, licensed under +the GNU General Public License, version 3 or later. +@end quotation +@end copying + +@dircategory Emacs +@direntry +* Card Games: (card-games). Play card games in Emacs (text and SVG). +@end direntry + +@titlepage +@title Card Games for Emacs +@subtitle A collection of solitaire, two-player, and partnership games +@subtitle for GNU Emacs, version @value{VERSION} +@author Corwin Brust +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@contents + +@ifnottex +@node Top +@top Card Games for Emacs + +@insertcopying +@end ifnottex + +@menu +* Introduction:: What this package is. +* Installation:: Getting it, and starting to play. +* The Game Menu:: The chooser, and its controls. +* Playing:: Controls shared by every game. +* Customization:: Colours, cards, keys, and opponents. +* Solitaire Games:: One-player patiences. +* Trick-Taking Games:: Hearts, Spades, Bridge, and friends. +* Rummy Games:: Melds, knocks, and books. +* Matching Games:: Go Fish and Old Maid. +* Capturing Games:: Scopa and Casino. +* Climbing Games:: President and Spite & Malice. +* Cribbage:: The pegging game, to 121. +* Russian Bank:: The two-player duel. +* Networked Play:: Live 500 over the network. +* Credits:: Sources and thanks. +* License:: Terms for this manual. +* Index:: Commands, variables, and concepts. +@end menu + +@node Introduction +@chapter Introduction + +@cindex introduction +@cindex about +Card Games for Emacs is a collection of more than thirty card games --- +solitaires, two-player games, and four-handed partnership games --- +played entirely inside GNU Emacs. + +@ifnotinfo +@center @image{images/klondike, , , Klondike solitaire in Emacs, png} +@end ifnotinfo + +Every game draws itself two ways. In a terminal it uses plain UNICODE +text, so it works anywhere Emacs runs, including over @command{ssh} and +on a phone. On a graphical display it draws proper SVG cards, with a +felt table for the four-handed games. You can switch between these +treatments at any time (@pxref{Customization}). + +You play with the keyboard, the mouse, or both. A highlighted cursor +marks the card or pile you are about to act on; the arrow keys move it +and @key{RET} acts. Clicking a card does the same thing. + +@cindex families of games +The games are grouped into families that share an engine and a feel: + +@table @asis +@item Solitaire +Klondike, FreeCell, Spider, Yukon, Canfield, Forty Thieves, Scorpion, +Golf, TriPeaks, Pyramid, Gaps (Montana), and Hell's Half-Acre, plus a +one-player Russian Bank. @xref{Solitaire Games}. + +@item Trick-taking +500, Hearts, Spades, Whist, Oh Hell, Euchre, Pitch, Briscola, and +Contract Bridge. @xref{Trick-Taking Games}. + +@item Rummy +Gin Rummy, Rummy, Rummy 500, and the partnership game Hand & Foot. +@xref{Rummy Games}. + +@item Matching, capturing, and climbing +Go Fish and Old Maid; Scopa and Casino; President, Spite & Malice, and +Cribbage. + +@item Two-player Russian Bank +The competitive duel, Crapette, against the computer. @xref{Russian +Bank}. +@end table + +To start, type @kbd{M-x card-game}. + +@node Installation +@chapter Installation + +@cindex installation +@cindex requirements +Card Games for Emacs needs GNU Emacs 26.1 or newer. SVG display +additionally needs an Emacs built with image support (any modern +graphical Emacs has it); in a terminal the games fall back to text. + +@menu +* From a package archive:: The easy way. +* From source:: Cloning the repository. +* Building:: Compiling, testing, and the manual. +@end menu + +@node From a package archive +@section From a package archive + +@cindex ELPA +When the package is available from a package archive, install it the +usual way with @kbd{M-x package-install @key{RET} card-games @key{RET}}. +The autoloads let you run @kbd{M-x card-game} straight away. + +@node From source +@section From source + +@cindex source, installing from +Clone the repository from @url{https://code.bru.st/corwin/card-game.el}, +add it to your @code{load-path}, and load the umbrella file: + +@example +(add-to-list 'load-path "/path/to/card-game.el") +(require 'card-games) +@end example + +@noindent +Then @kbd{M-x card-game}. You can also build an installable tarball with +@kbd{make package} and install it with @kbd{M-x package-install-file}. + +@node Building +@section Building + +@cindex Makefile +@cindex tests +The @file{Makefile} provides the usual developer targets: + +@table @code +@item make compile +Byte-compile all the sources. +@item make test +Run the ERT test suite. +@item make info +Build this manual (also @code{make html} and @code{make pdf}). +@item make package +Build the installable @file{.tar}. +@end table + +@node The Game Menu +@chapter The Game Menu + +@findex card-game +@findex card-games +@cindex chooser +@cindex menu +@kbd{M-x card-game} (also available as @code{card-games}) opens the +chooser: a buffer listing every game with a one-line description. Move +between games with @key{TAB} and @kbd{S-@key{TAB}}, or @kbd{n} and +@kbd{p}, and press @key{RET} --- or click a game's name --- to start it. +@kbd{q} buries the menu. + +Two controls sit at the top of the list. + +@table @asis +@item AI opponents +@vindex cg-ai-level +Sets how hard the computer plays: @code{easy}, @code{normal}, or +@code{hard}. Click it to cycle, or use @kbd{M-x card-games-set-ai-level}. +@xref{Opponents}. + +@item Cards +@vindex card-games-treatment +Chooses how the games are drawn: @code{text}, @code{svg}, or @code{full}. +Click it to cycle, or use @kbd{M-x card-games-set-treatment}. +@xref{Display}. +@end table + +@node Playing +@chapter Playing + +@cindex controls, shared +@cindex keys +Each game has its own rules and a few keys of its own, but the basic +controls are the same everywhere. Press @kbd{?} in any game to see that +game's keys in the echo area. + +@table @kbd +@item @key{LEFT} @key{RIGHT} @key{UP} @key{DOWN} +Move the highlighted cursor between cards and piles. +@item @key{RET} +Act on the cursor: pick up or drop a card, play it, or select it. Many +games also accept @key{SPC}. +@item mouse-1 +Click a card or pile to do the same as moving the cursor there and +acting. +@item f +Where there are foundations, send the chosen card to one. +@item u +Undo the last move, in the games that support it. +@item n +Deal a new game. +@item g +Redraw the board. +@item ? +Describe the game's controls. +@item q +Leave the game and return to the menu (@pxref{The Game Menu}). +@end table + +@cindex zoom +@vindex cg-card-scale +On a graphical display, @kbd{+} and @kbd{-} (and @kbd{=}) make the cards +larger and smaller, and @kbd{0} resets the size. Emacs's own +@code{text-scale-adjust} works too. Many boards show a one-line legend +of the current game's keys along the bottom. + +@node Customization +@chapter Customization + +@cindex customization +All the options live in the @code{card-games} customization group; type +@kbd{M-x customize-group @key{RET} card-games @key{RET}} to browse them. +The most useful are collected here. + +@menu +* Display:: Text, SVG cards, or a full table. +* Cards and colours:: Backs, decks, glyphs, and themes. +* The Emacs emblem:: Which logo the full table shows. +* Keys:: Emacs or classic movement. +* Opponents:: How hard the computer plays. +@end menu + +@node Display +@section Display + +@vindex card-games-treatment +@findex card-games-set-treatment +@kbd{M-x card-games-set-treatment} switches every game between three +treatments: @code{text} draws UNICODE cards, @code{svg} draws SVG cards, +and @code{full} additionally uses the full-window SVG table for the games +that have one (Gaps and 500). You can also cycle it from the menu +(@pxref{The Game Menu}). A change takes effect the next time a game is +drawn; press @kbd{g} to redraw an open game. + +@vindex cg-card-scale +The card size follows @code{cg-card-scale} and the zoom keys +(@pxref{Playing}). + +@node Cards and colours +@section Cards and colours + +@vindex cg-svg-card-back +@findex cg-svg-shuffle-card-back +@cindex card backs +The pattern on a face-down card is @code{cg-svg-card-back}. Besides +@code{dots}, @code{rings}, and @code{solid} there are the drawn patterns +@code{lattice}, @code{waves}, and @code{diamond}, and four backs stamped +with an Emacs logo: @code{emacs}, @code{emacs-classic}, @code{gnu}, and +@code{splash}. The default, @code{random}, chooses a back for the +session; @kbd{M-x cg-svg-shuffle-card-back} rolls a new one, and +reopening the menu also re-rolls. + +@vindex cg-svg-four-color +@cindex four-colour deck +With @code{cg-svg-four-color} non-@code{nil}, clubs are drawn green and +diamonds blue, so all four suits are told apart by colour. + +@vindex cg-symbols +@cindex suit glyphs +@code{cg-symbols} maps each suit to the glyph used for it, in both the +text and the SVG cards; customize it to use the outlined suits +@samp{♤ ♧ ♢ ♡}, say, instead of the filled ones. + +@findex card-games-set-theme +@vindex card-games-themes +@cindex themes +@kbd{M-x card-games-set-theme} applies a colour preset --- +@code{classic}, @code{dark}, or @code{contrast} --- setting the felt +colour, the card back, and the highlight together. The individual +colours (@code{cg-svg-highlight-color}, @code{cg-bid-felt-color}, and the +rest) can also be set on their own. + +@vindex cg-cursor-type +@cindex cursor +Card buffers hide the text cursor by default, since you act on the +highlighted card rather than on point; @code{cg-cursor-type} can bring it +back. + +@node The Emacs emblem +@section The Emacs emblem + +@vindex cg-svg-emacs-logo +@cindex logo +The full-window tables (500 and Gaps) show an Emacs emblem in a corner. +@code{cg-svg-emacs-logo} chooses it: @code{modern} (the current Emacs +icon, the default), @code{classic} (the older icon), @code{gnu} (a GNU +head), @code{splash} (the startup image), @code{drawn} (a small built-in +emblem), or @code{none}. The image choices embed a logo that ships with +your Emacs, falling back to the drawn emblem when it cannot be found. + +@node Keys +@section Keys + +@vindex cg-keys +@cindex key scheme +@code{cg-keys} selects a movement scheme. @code{emacs} (the default) +follows Emacs conventions --- the arrow keys move and @key{RET} acts. +@code{classic} additionally enables @kbd{h} @kbd{j} @kbd{k} @kbd{l} and +@key{SPC}. A change takes effect the next time a game starts. + +@node Opponents +@section Opponents + +@vindex cg-ai-level +@findex card-games-set-ai-level +@cindex difficulty +@code{cg-ai-level} sets how hard the computer plays: @code{easy}, +@code{normal}, or @code{hard}. Russian Bank plays all three levels; the +trick-taking games play a random legal card on @code{easy} and their +usual game otherwise. Other games do not yet consult it. Set it with +@kbd{M-x card-games-set-ai-level} or from the menu. + +@node Solitaire Games +@chapter Solitaire Games + +@cindex solitaire +@cindex patience +The one-player games. Most are @dfn{tableau builders} that share the +same controls; the pile games (Golf, TriPeaks, Pyramid) and the gaps +games (Gaps, Hell's Half-Acre) play a little differently and say so in +their own sections. + +@cindex solitaire, controls +The shared keys for the tableau builders are: + +@table @kbd +@item @key{LEFT} @key{RIGHT} +Move the cursor between piles. +@item @key{RET} @r{(or} @key{SPC}@r{)} +Pick up the card, or the movable run, under the cursor; press again on a +destination to drop it. On the stock, deal cards to the waste (or, when +the stock is empty, recycle the waste where the game allows). +@item f +Send the card under the cursor to a foundation. +@item a +Auto-play the cards that plainly belong on the foundations. +@item u +Undo the last move. +@item n @r{/} g @r{/} ? @r{/} q +New deal, redraw, describe the keys, and return to the menu. +@end table + +@noindent +With @code{cg-keys} set to @code{classic} the vi keys @kbd{h} @kbd{j} +@kbd{k} @kbd{l} move as well (@pxref{Keys}). + +@menu +* Klondike:: The classic. +* FreeCell:: A game of pure skill. +* Spider:: Two decks; clear eight suited runs. +* Yukon:: Klondike, but move any group. +* Canfield:: A reserve and a wrapping base rank. +* Forty Thieves:: Two decks, no redeal, and hard. +* Scorpion:: Free four buried runs. +* Russian Bank Solitaire:: Houses and a feeding reserve. +* Golf:: One rank up or down. +* TriPeaks:: Golf with wrapping chains. +* Pyramid:: Remove pairs that sum to thirteen. +* Gaps:: Order the rows through the gaps. +* Hell's Half-Acre:: Gaps, built the other way. +@end menu + +@node Klondike +@section Klondike + +@findex cg-klondike +@cindex Klondike +The classic. Build the four foundations up in suit, Ace to King. Seven +columns hold a descending, alternating-colour tableau; move a card or an +ordered run onto the next-higher card of the other colour, and fill an +empty column with a King (or a King-headed run). Turn cards from the +stock to the waste --- one at a time by default --- and recycle the +waste when the stock runs out. + +@vindex cg-sol-klondike-draw +Set @code{cg-sol-klondike-draw} to @code{3} for the harder +turn-three variant. + +@cindex strategy, Klondike +@strong{Strategy.} Uncover face-down cards before anything else, and +keep a column open for a King. Do not rush low cards to the foundations +if you may still need them to receive tableau cards. + +@node FreeCell +@section FreeCell + +@findex cg-freecell +@cindex FreeCell +Every card is dealt face up into eight columns, and four @dfn{free cells} +each hold a single card. Build the tableau down in alternating colour +and the foundations up in suit. How large a run you can shift at once +depends on how many cells and empty columns are free. Almost every deal +can be won --- this is a game of skill, not luck. + +@strong{Strategy.} Plan several moves ahead, free the aces early, and +resist filling all four cells; an empty column is worth more than a full +cell. + +@node Spider +@section Spider + +@findex cg-spider +@cindex Spider +Two decks, ten columns, no separate foundations. Build down regardless +of suit, but only a same-suit run moves as a block. Complete a +King-to-Ace run in one suit and it is lifted off the table; clear all +eight to win. Press @key{RET} on the stock to deal one card to every +column --- but only when no column is empty. + +@strong{Strategy.} Build in suit whenever you have the choice, and empty +a column as soon as you can: it is the key to untangling the rest. + +@node Yukon +@section Yukon + +@findex cg-yukon +@cindex Yukon +Klondike's layout, dealt mostly face up, with one liberating difference: +you may move @emph{any} face-up card, together with everything piled on +top of it, onto a card one higher of the other colour --- the group need +not be in order. There is no stock. + +@strong{Strategy.} Expose the face-down cards; the freedom to lift +buried groups wins many deals that Klondike would lose. + +@node Canfield +@section Canfield + +@findex cg-canfield +@cindex Canfield +A thirteen-card @dfn{reserve}, four tableau columns, and a stock dealt +three at a time. The first card sets the base rank for the foundations, +which build up in suit and wrap from King round to Ace. The tableau +builds down in alternating colour, and an empty column refills from the +reserve. + +@strong{Strategy.} Clear the reserve --- it is the bottleneck --- and +keep the wrapping base rank in mind when you choose what to bank. + +@node Forty Thieves +@section Forty Thieves + +@findex cg-forty-thieves +@cindex Forty Thieves +Two decks, ten columns, eight foundations. The tableau builds down +@emph{in suit} and moves one card at a time; the foundations build up in +suit; the stock deals to the waste with no redeal. A hard, skilful game. + +@strong{Strategy.} Be patient, keep the waste short, and avoid burying +the low cards you will need. + +@node Scorpion +@section Scorpion + +@findex cg-scorpion +@cindex Scorpion +Seven columns, built down in suit. As in Yukon you may move any card +with everything on top of it, ordered or not. Free four King-to-Ace runs +to win. A small stock deals onto the first columns when you are stuck. + +@strong{Strategy.} Dig the buried low cards out early, and think about +which King you can afford to complete first. + +@node Russian Bank Solitaire +@section Russian Bank + +@findex cg-russian-bank +@cindex Russian Bank, solitaire +The one-player patience: eight @dfn{houses} built down in alternating +colour, four foundations built up in suit from the Ace, and a +thirteen-card reserve that feeds an empty house. (For the competitive +two-player game, @pxref{Russian Bank}.) + +@strong{Strategy.} Play to the foundations first and empty the reserve; +the houses are just working space. + +@node Golf +@section Golf + +@findex cg-golf +@cindex Golf +A layout of thirty-five cards over a single waste pile. Play any exposed +card onto the waste when it is one rank above @emph{or} below the waste's +top card; the sequence does @emph{not} wrap, so nothing follows a King. +Turn a fresh card from the stock when you are stuck. Clear the whole +layout to win. + +@cindex controls, pile games +Use the arrow keys or the mouse to choose a card, @key{RET} to play it +(or, on the stock, to deal), @kbd{u} to undo, and @kbd{n} for a new deal. + +@strong{Strategy.} Look for long up-and-down chains before you spend a +card from the stock. + +@node TriPeaks +@section TriPeaks + +@findex cg-tripeaks +@cindex TriPeaks +Golf played over three overlapping peaks, with one change: the sequence +@emph{wraps}, so an Ace follows a King and a King follows an Ace. That +lets you run long chains across the peaks. Clear all three to win. The +keys are the same as Golf. + +@strong{Strategy.} Plan the chain that uncovers the most cards, and hold +the stock in reserve for when the board truly stalls. + +@node Pyramid +@section Pyramid + +@findex cg-pyramid +@cindex Pyramid +A twenty-eight-card pyramid. Remove pairs of exposed cards whose ranks +sum to thirteen --- Ace counts 1, Jack 11, Queen 12, and a King is 13, so +a King leaves on its own. Mark one card and then its partner to remove +them, and deal from the stock for more matches. Clear the pyramid to +win. + +@strong{Strategy.} Free the cards that block two others at once, and do +not strand a card whose only partner is already gone. + +@node Gaps +@section Gaps + +@findex cg-montana +@findex cg-gaps +@cindex Gaps +@cindex Montana +Also called Montana. The pack is dealt into four rows with gaps between +the cards. Each row is built as one suit, with a Two at the head, rising +Two, Three, @dots{}, up to the King. Move a card into a gap when it +continues the row --- the card one higher, in the same suit, than the +card to the gap's left. Fillable gaps are ringed and marked with a green +@samp{+}. + +When no move remains, @kbd{r} reshuffles the misplaced cards for another +try; you get a limited number of these redeals. @kbd{v} toggles the +full-window layout. Order every row to win. + +@strong{Strategy.} Open the gaps that let a Two, then a Three, begin +each row, and spend redeals only when you are truly stuck. + +@node Hell's Half-Acre +@section Hell's Half-Acre + +@findex cg-hells-half-acre +@cindex Hell's Half-Acre +The same game as Gaps, built the other way: a King anchors the head of +each row and the rows descend King, Queen, @dots{}, down to the Two. The +controls are identical. + +@node Trick-Taking Games +@chapter Trick-Taking Games + +@cindex trick-taking +@cindex tricks +In these games the four players each play a card to the @dfn{trick}, and +the highest card (or the highest trump) wins it. You sit South; the +other three seats are computer players, and for the partnership games +North is your partner. + +@ifnotinfo +@center @image{images/hearts, , , A hand of Hearts, png} +@end ifnotinfo + +@cindex trick games, controls +Most of them share these controls: + +@table @kbd +@item @key{LEFT} @key{RIGHT} +Choose a card in your hand. +@item @key{RET} @r{(or} @key{SPC}@r{)} +Play the chosen card. During the Hearts pass it instead marks a card. +@item p +Confirm the Hearts pass, once three cards are marked. +@item n @r{/} g @r{/} ? @r{/} q +New match, redraw, describe the keys, and return to the menu. +@end table + +@noindent +When a game calls for a bid, you are prompted for it in the minibuffer. +@xref{Opponents}, to set how hard the computer plays. 500 and Bridge +have richer boards and a few keys of their own, noted below. + +@menu +* Five Hundred: 500. Partnership bidding to 500. +* Hearts:: Avoid the hearts and the black lady. +* Spades:: Bid your tricks; spades are trump. +* Whist:: Fixed trump, no bidding. +* Oh Hell:: Bid exactly, on shrinking hands. +* Euchre:: Bowers, and going alone. +* Pitch:: Set the trump with your lead. +* Briscola:: No follow; capture the points. +* Bridge: Contract Bridge. The auction, the dummy, the rubber. +@end menu + +@node 500 +@section 500 + +@findex cg-bid +@cindex 500 +Australia's national card game, and the flagship of this collection. You +and North play against East and West. After the deal there is an +auction: each side bids a number of tricks (six to ten) and a trump --- +or Misère, to take no tricks at all. The high bidder takes the five-card +@dfn{kitty}, then discards five cards, and play begins. + +The trump order runs Joker (highest), then the Jack of trump (right +bower) and the other Jack of the same colour (left bower), then Ace down +to Four. Score the Avondale table for making or breaking the contract. +A side wins by reaching 500 on a contract it made (the ``front door''), +and loses by falling to @minus{}500 (the ``back door''). + +@table @kbd +@item b +Make a bid; @kbd{p} passes. +@item @key{RET} +Select the card, or the kitty card, under the pointer. +@item x +Discard the five cards you have marked for the kitty. +@item v +Toggle the full-window table. +@item M-@key{UP} M-@key{DOWN} +Scroll the message log (the mouse wheel works too). +@item n @r{/} ? @r{/} q +New game, help, and the menu. +@end table + +@noindent +500 can also be played live against other people over the network. +@xref{Networked Play}. + +@strong{Strategy.} Bid what your trumps and side-suit aces can actually +make; take the kitty only when it can complete a contract; and keep +Misère for a hand too weak to win a single trick. + +@node Hearts +@section Hearts + +@findex cg-hearts +@cindex Hearts +An avoidance game with no trump. Every heart costs one point and the +Queen of Spades costs thirteen, and you want as few as possible. Before +each hand you pass three cards to another player --- left, right, across, +then a hand with no passing, and around again. The Two of Clubs leads +the first trick, on which no points may be played. + +Take @emph{all} the points in a hand and you ``shoot the moon'': the +twenty-six are charged to everyone else instead. When a player reaches +100 the game ends and the lowest score wins. + +@strong{Strategy.} Void a suit so you can throw the Queen or a heart into +it; keep track of who might hold the Queen; and only chase the moon with a +hand that cannot be stopped. + +@node Spades +@section Spades + +@findex cg-spades +@cindex Spades +Spades are always trump. Each player bids the number of tricks they +expect to win, and the two partners' bids are added: your side must make +that combined total. Extra tricks (``bags'') score a point each, but +every tenth bag costs you 100. A bid and made @dfn{Nil} --- no tricks at +all --- scores 100, or loses 100 if broken. Play to 500. + +@strong{Strategy.} Count your sure winners before you bid; go Nil only +with low spades and short, duckable side suits; and protect your +partner's Nil when you can. + +@node Whist +@section Whist + +@findex cg-whist +@cindex Whist +The old English ancestor of Bridge, with no bidding. The dealer's last +card is turned for trump. Follow the suit led if you can; trumps beat the +plain suits. Each side counts one point for every trick it wins beyond +the @dfn{book} of six, and the first side to five wins. + +@strong{Strategy.} Lead from your longest suit, draw the opponents' +trumps, and remember the cards that have gone. + +@node Oh Hell +@section Oh Hell + +@findex cg-ohhell +@cindex Oh Hell +The hand shrinks every deal, from seven cards down to one, and a card is +turned for trump. Each player bids the @emph{exact} number of tricks +they will take and scores a bonus only for hitting it precisely --- one +over or under and the bonus is lost. Play the fixed run of rounds; it is +every player for themselves. + +@strong{Strategy.} On the small hands a high card is a sure trick and a +low one a sure miss; do not be afraid to bid zero and duck everything. + +@node Euchre +@section Euchre + +@findex cg-euchre +@cindex Euchre +A brisk 24-card game (Nine to Ace). The Jack of the trump suit (the +@dfn{right bower}) and the other Jack of the same colour (the @dfn{left +bower}) are the two highest trumps. A card is turned up, and you may +@dfn{order it up} as trump or, later, name a different suit. The side +that names trump must take three of the five tricks. Partnership to 10. + +You are asked whether to order up the turned card (@kbd{y} or @kbd{n}) +and, if bidding comes round again, to name a trump. + +@strong{Strategy.} Order up with three trumps, or two trumps and an +off-suit Ace; a very strong hand can be played ``alone'' for a bigger +score. + +@node Pitch +@section Pitch + +@findex cg-pitch +@cindex Pitch +Auction Pitch. Players bid for the right to @dfn{pitch}, and the +pitcher's first lead sets the trump suit. Thereafter follow the led suit +if you can, but you may always trump instead. Four points are at stake +each hand --- High, Low, Jack, and Game --- and the first to seven wins. + +@strong{Strategy.} Bid only on a solid trump holding, ideally with the +Jack or with both the highest and lowest trumps. + +@node Briscola +@section Briscola + +@findex cg-briscola +@cindex Briscola +A 40-card Italian game. One card is turned to fix the trump, the +@dfn{briscola}, and --- unusually --- there is no need to follow suit: you +may play any card to any trick. Winning a trick captures its cards, and +the Aces and Threes carry the most points. Capture the points; the first +to 61 of the 120 wins. + +@strong{Strategy.} Save your briscole to capture the opponents' Aces, and +lead worthless cards to coax points out of them. + +@node Contract Bridge +@section Bridge + +@findex cg-bridge +@cindex Bridge +@cindex Contract Bridge +The full game: the auction, then the play with the dummy exposed, and +rubber scoring. You sit South and partner North against East and West. + +In the auction you compose a call and make it: @key{UP} and @key{DOWN} +change the level, @key{LEFT} and @key{RIGHT} change the strain, @key{RET} +bids it, @kbd{p} passes, and @kbd{d} doubles. When the auction ends, the +declaring side's first player is declarer and their partner is the dummy, +whose hand is turned face up; the declarer plays both hands, so you play +the dummy from your seat when it is the dummy's turn. Making your +contract scores below the line toward game and the rubber. + +@table @kbd +@item arrows @r{and} @key{RET} +Compose and make a call (auction), or choose and play a card (play). +@item p @r{/} d +Pass and double, during the auction. +@item n @r{/} g @r{/} q +Next deal, redraw, and the menu. +@end table + +@strong{Strategy.} Count your high-card points, look for an eight-card +trump fit with your partner, and plan the whole hand --- drawing trumps, +setting up a long suit --- before you play to the first trick. + +@node Rummy Games +@chapter Rummy Games + +@cindex rummy +@cindex melds +Draw a card --- from the stock or the discard pile --- then throw one +away, and work your hand into @dfn{melds}: @dfn{sets} of three or four of +a kind, and @dfn{runs} of three or more cards in one suit. + +@menu +* Gin Rummy:: Knock with little deadwood. +* Rummy:: Meld your whole hand to go out. +* Rummy 500:: Score the cards you lay down. +* Hand & Foot:: The partnership Canasta cousin. +@end menu + +@node Gin Rummy +@section Gin Rummy + +@findex cg-gin +@cindex Gin Rummy +@cindex deadwood +@cindex knock +Two-handed, with ten-card hands. Draw from the stock (@kbd{s}) or take +the up-card (@kbd{t}), then discard with @key{RET}. When your unmatched +cards --- your @dfn{deadwood} --- add up to ten points or fewer you may +@dfn{knock} (@kbd{k}) and end the hand; with no deadwood at all you go +@dfn{gin} for a bonus. Your opponent then lays their own deadwood off +onto your melds. First to 100 points wins. + +@strong{Strategy.} Keep cards that could join more than one meld, knock +early against a slow hand, and note which cards your opponent takes. + +@node Rummy +@section Rummy + +@findex cg-rummy-basic +@cindex Rummy +Draw and discard, and lay your melds down on the table. Mark the cards +of a meld with @key{SPC} and lay it down with @kbd{m}; add a single card +to a meld already on the table (yours or anyone's) with @kbd{l}. Get rid +of your whole hand to go out. + +@table @kbd +@item @key{SPC} @r{/} m @r{/} l +Mark cards, meld them, lay one off. +@item s @r{/} t @r{/} @key{RET} +Draw from the stock, take the up-card, discard. +@end table + +@strong{Strategy.} Do not meld too soon --- cards on the table cannot be +rearranged, and they tell your opponents what you hold. + +@node Rummy 500 +@section Rummy 500 + +@findex cg-rum500 +@cindex Rummy 500 +Rummy played for points over many hands, first past 500. You score the +cards you lay down and lose the ones left in your hand. Besides the top +of the discard pile you may take a card from deeper in it with @kbd{T}, +picking up that card and everything above it --- as long as you meld the +chosen card at once. Aces count 15. + +@strong{Strategy.} Chase the high-scoring cards, and raid the discard +pile when the reward outweighs the cards it puts in your hand. + +@node Hand & Foot +@section Hand & Foot + +@findex cg-handfoot +@cindex Hand and Foot +A partnership cousin of Canasta. You are dealt a @dfn{hand} and a +@dfn{foot}; play out the hand, then take up the foot. Build @dfn{books} +of seven cards of a rank --- @dfn{clean} with no wild cards, or +@dfn{dirty} with some. Twos and Jokers are wild; threes never meld. +Draw two cards with @kbd{s}, or pick up the whole discard pile with +@kbd{p} by melding its top card; meld (@kbd{m}), lay off (@kbd{l}), and +discard (@key{RET}). Your side goes out once it has two complete books +and you have emptied your foot. + +@strong{Strategy.} Hoard wild cards for clean books, and do not go out +before your partner has played their foot. + +@node Matching Games +@chapter Matching Games + +@cindex matching games +Two light games of collecting and shedding, good for a quick sit-down. + +@menu +* Go Fish:: Ask for ranks; collect books of four. +* Old Maid:: Shed pairs; dodge the odd Queen. +@end menu + +@node Go Fish +@section Go Fish + +@findex cg-go-fish +@cindex Go Fish +@cindex books +Collect @dfn{books} of four of a kind. On your turn pick a rank you hold +--- click one of the rank buttons, or move the cursor to a card and it +selects that rank --- then press @kbd{1}, @kbd{2}, @kbd{3}, or @kbd{4} to +ask that player for it. If they have any they must hand them over and you +ask again; if not, you ``go fish'' and draw from the stock. Complete the +most books to win. + +@strong{Strategy.} Remember what everyone has asked for; it tells you +who holds what. + +@node Old Maid +@section Old Maid + +@findex cg-old-maid +@cindex Old Maid +One Queen is set aside, so a single Queen is left without a partner. +Throw out every pair in your hand, then draw a card, unseen, from the next +player: move over their face-down cards with the arrows (or click one) and +press @key{RET}. Whoever is left holding the odd Queen at the end loses. + +@strong{Strategy.} Mostly luck --- but do not let your reactions reveal +where the odd Queen is hiding. + +@node Capturing Games +@chapter Capturing Games + +@cindex capturing games +@cindex fishing games +Play a card from your hand to the table; if it matches, it @dfn{captures} +table cards and takes them for scoring. Press @key{RET} to play the card +under the cursor. + +@menu +* Scopa:: Sweep the table for a scopa. +* Casino:: Big and little casino, and the sweeps. +@end menu + +@node Scopa +@section Scopa + +@findex cg-scopa +@cindex Scopa +A 40-card Italian game. The card you play captures a single table card of +the same value, or a set of table cards that add up to its value. +Clearing the whole table at a stroke is a @dfn{scopa} and scores a bonus. +At the end of each round you score for taking the most cards, the most +coins, the Seven of Coins (the @dfn{sette bello}), and the @dfn{primiera}; +play to 11. + +@strong{Strategy.} Watch the running total on the table, and try to leave +your opponent unable to make the capture that would sweep it. + +@node Casino +@section Casino + +@findex cg-casino +@cindex Casino +The 52-card cousin of Scopa. Number cards capture by value as in Scopa; +face cards capture only by matching rank. Score the @dfn{big casino} +(the Ten of Diamonds), the @dfn{little casino} (the Two of Spades), the +Aces, and the most cards and most spades; play to 21. + +@strong{Strategy.} Guard the two casinos and grab spades when you can --- +the little points decide close games. + +@node Climbing Games +@chapter Climbing Games + +@cindex climbing games +@cindex shedding games +Be the first to get rid of your cards by playing something that beats +what is already on the table. + +@menu +* President:: First out rules; last out scrubs. +* Spite & Malice:: Race to empty your goal pile. +@end menu + +@node President +@section President + +@findex cg-president +@cindex President +@cindex Scum +Also called Scum. The leader plays one to four cards of a rank; the next +player must answer with the same number of a @emph{higher} rank, or pass +--- and here the Two is the highest card of all. Whoever empties their +hand first is President; the last one out is the Scum, and before the next +deal the Scum must hand the President their best cards. + +To lead more than one card of a rank, give a numeric prefix first --- +@kbd{C-u 2 @key{RET}} leads a pair. @kbd{p} passes. + +@strong{Strategy.} Shed your low cards while the lead is cheap, and save +your Twos and your pairs to grab the lead back when it matters. + +@node Spite & Malice +@section Spite & Malice + +@findex cg-spite +@cindex Spite and Malice +@cindex Cat and Mouse +Race to empty your @dfn{goal} pile. Four shared centre piles are built up +from Ace to Queen, and Kings are wild. On your turn play a card from your +hand with @key{RET}, the top of your goal pile with @kbd{G}, or the top of +one of your four discard piles with @kbd{1}--@kbd{4}; then end your turn by +discarding a hand card with @kbd{d}. The first to clear their goal pile +wins. + +@strong{Strategy.} Spend a wild King to reach the next goal card, and +keep your discard piles in order so you can unload them in turn. + +@node Cribbage +@chapter Cribbage + +@findex cg-cribbage +@cindex Cribbage +@cindex pegging +@cindex the crib +The classic two-player game, pegged to 121. Each deal has three parts. + +First, both players lay two cards face down to the dealer's @dfn{crib}: +mark them with @key{SPC} and confirm with @kbd{m}. A @dfn{starter} card is +then cut. Next comes @dfn{the play}: take turns laying cards (@key{RET}), +scoring as the running total reaches fifteen and thirty-one and for pairs +and runs made along the way. Finally, @dfn{the show} scores each hand and +then the crib for its fifteens, pairs, runs, flushes, and @dfn{his nobs} +(the Jack of the starter's suit). First to 121 wins. + +@strong{Strategy.} Weigh a strong hand against what you give away to the +crib, and always be counting toward the next fifteen and thirty-one. + +@node Russian Bank +@chapter Russian Bank + +@findex cg-crapette +@findex cg-russian-bank-duel +@cindex Russian Bank +@cindex Crapette +Russian Bank, also called Crapette, is the competitive two-player +ancestor of the solitaire (@pxref{Russian Bank Solitaire}). You (South) +play against the computer (North), each with your own 52-card pack. + +In the centre sit eight @dfn{foundations}, built up by suit from the Ace, +and eight @dfn{houses}, built down in alternating colour; both are common +ground either player may build on. Each of you also has a thirteen-card +@dfn{reserve} (its top card face up), a @dfn{waste}, and a face-down +@dfn{hand}. You win by getting rid of every card in your reserve, hand, +and waste. + +On your turn you make as many legal moves as you like: + +@itemize @bullet +@item +move the top of your reserve, your waste, or any house onto a foundation +or a house; +@item +move a whole @dfn{sequence} (a run built down in alternating colours) from +one house to another --- but only when there are enough empty houses to +have shifted it a card at a time; and +@item +@dfn{load} a card from your reserve or waste onto the opponent's reserve +or waste, when it is the same suit and one rank higher or lower. +@end itemize + +@cindex stop rule +@vindex cg-crapette-stops +@strong{Foundation priority and ``Stop''.} A card that can go to a +foundation must be played there before anything else. If you build a +house, load your opponent, turn a card, or end your turn while a +foundation play is waiting, your opponent calls ``Stop!'' and your turn +ends at once --- the piles that owe a foundation play are ringed to warn +you. Set @code{cg-crapette-stops} to @code{nil} for a gentler mode that +simply blocks the slip instead of ending your turn. + +When you can do no more, turn the top of your hand: if it fits somewhere +you keep going, otherwise it goes to your waste and your turn ends. + +@table @kbd +@item arrows +Move the cursor between piles. +@item @key{RET} @r{(or click)} +Pick up the card or run under the cursor; press again on a destination to +drop it. +@item f +Send the chosen card to a foundation. +@item [ @r{and} ] +Choose how many cards of a picked-up run to move onto an empty house. +@item @key{SPC} @r{(or} d@r{)} +Turn the top card of your hand. +@item e +End your turn. +@item u @r{/} n @r{/} q +Undo, new game, and the menu. +@end table + +@noindent +@code{cg-ai-level} sets how hard North plays (@pxref{Opponents}): on +@code{hard} it even looks a move ahead to rearrange the houses. + +@strong{Strategy.} Play to the foundations first, and empty your reserve +--- it is the pile you must clear to win. Load your spare cards onto your +opponent to slow them down, and keep an empty house free as room to shift +a sequence. + +@node Networked Play +@chapter Networked Play + +@findex cg-bid-host +@findex cg-bid-join +@cindex networked play +@cindex multiplayer +500 can be played live against other people over a TCP connection. One +player hosts the game and the others join it. + +@table @kbd +@item M-x cg-bid-host @key{RET} @var{port} @key{RET} +Start a game as the host. You take the South seat and wait for others to +connect. +@item M-x cg-bid-join @key{RET} @var{host} @key{RET} @var{port} @key{RET} @var{name} @key{RET} +Connect to a host at @var{host} and @var{port} under a display +@var{name}. +@end table + +@cindex lobby +The game starts automatically once four people have joined. The host may +also press @kbd{s} to start early, with the computer filling any empty +seats. + +@vindex cg-bid-shuffle-partners +With @code{cg-bid-shuffle-partners} non-@code{nil}, the joining players +are dealt randomly among the seats. Each player's view is rotated so +they sit South, so the ordinary controls (@pxref{500}) work unchanged. +Play is turn-based and the host is authoritative; a client can join from +anywhere Emacs can open a network connection. + +@node Credits +@chapter Credits + +@cindex credits +@cindex acknowledgements +Card Games for Emacs was written by Corwin Brust. + +The rules of the trickier games were checked against public references, +chiefly Wikipedia and John McLeod's card-game site, Pagat +(@url{https://www.pagat.com/}). Thanks are due to the playtesters whose +feedback shaped the controls and the display. + +@cindex Claude +Much of the code, and this manual, were written by Corwin Brust in +collaboration with Claude, an AI assistant made by Anthropic, over a +series of pair-programming sessions. + +@node License +@appendix License + +This manual is licensed under the Creative Commons Attribution 4.0 +International License. To view a copy of this license, visit +@url{https://creativecommons.org/licenses/by/4.0/}. + +@node Index +@unnumbered Index + +@printindex cp + +@bye diff --git a/doc/images/hearts.png b/doc/images/hearts.png new file mode 100644 index 0000000..e9f7c18 Binary files /dev/null and b/doc/images/hearts.png differ diff --git a/doc/images/klondike.png b/doc/images/klondike.png new file mode 100644 index 0000000..fdfb195 Binary files /dev/null and b/doc/images/klondike.png differ diff --git a/doc/version.texi b/doc/version.texi new file mode 100644 index 0000000..0380b8a --- /dev/null +++ b/doc/version.texi @@ -0,0 +1,3 @@ +@set VERSION 1.0.91 +@set UPDATED 1 July 2026 +@set YEAR 2026 diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100644 index 0000000..bf5662b --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,37 @@ +#!/usr/bin/env sh +# hooks/pre-commit -- regenerate README.md from README.org on commit. +# +# When README.org (or build.el) is part of the commit, re-run the batch +# Org -> Markdown export and stage the refreshed README.md so it travels +# in the same commit. GitHub and MELPA render Markdown better than Org. +# +# Lightweight by design: no sentinel file, no post-commit --amend. If +# nothing relevant changed, the hook exits immediately. +# +# Install: make hooks (or: ln -s ../../hooks/pre-commit .git/hooks/pre-commit) +# +# Set EMACS in the environment if the Emacs binary is not named "emacs" +# on the PATH git sees (common on Windows/MSYS2). + +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +# Only act when README.org or the exporter itself is staged. +if ! git diff --cached --name-only | grep -Eq '^(README\.org|build\.el)$'; then + exit 0 +fi + +EMACS_BIN="${EMACS:-emacs}" +if ! command -v "$EMACS_BIN" >/dev/null 2>&1; then + echo "pre-commit: WARNING: '$EMACS_BIN' not found; README.md not regenerated." >&2 + echo "pre-commit: set EMACS to your Emacs binary, or run 'make readme'." >&2 + exit 0 +fi + +echo "pre-commit: regenerating README.md from README.org" +"$EMACS_BIN" -Q --batch -l build.el + +git add README.md +exit 0 diff --git a/test/card-games-tests.el b/test/card-games-tests.el index e0a9165..ccb113b 100644 --- a/test/card-games-tests.el +++ b/test/card-games-tests.el @@ -760,6 +760,48 @@ (let ((cg-crap--recording nil)) (cg-crap--ai-play g)) (should (eq (cg-get g :winner) 1)))) ; plays its last card, wins +(ert-deftest cgt-crap-ai-enabling () + ;; reserve 5D cannot be placed until the AI shifts 6H off 6S (crafty rearrange) + (let ((g (cg-crap--deal (cg-crapette-game)))) + (aset (cg-get g :reserve) 1 (list '(2 . 4))) ; 5 of diamonds + (aset (cg-get g :waste) 1 nil) (aset (cg-get g :hand) 1 nil) + (aset (cg-get g :reserve) 0 nil) (aset (cg-get g :waste) 0 nil) + (aset (cg-get g :found) 0 nil) + (aset (cg-get g :houses) 0 (list '(0 . 5) '(3 . 5))) ; 6S (bottom), 6H (top) + (aset (cg-get g :houses) 1 (list '(1 . 6))) ; 7C + (dotimes (k 6) (aset (cg-get g :houses) (+ 2 k) (list '(2 . 6)))) ; 7D fillers, none empty + (cg-put g :turn 1) + (let ((cg-ai-level 'hard) (cg-crap--recording nil)) (cg-crap--ai-play g)) + (should (null (cg-crap--reserve g 1))))) ; hard unstuck and unloaded + +(ert-deftest cgt-crap-ai-loads-you () + ;; reserve 6D loads onto your reserve top 7D; the AI should prefer that + (let ((g (cg-crap--deal (cg-crapette-game)))) + (aset (cg-get g :reserve) 1 (list '(2 . 5))) ; 6D + (aset (cg-get g :waste) 1 nil) (aset (cg-get g :hand) 1 nil) + (aset (cg-get g :reserve) 0 (list '(2 . 6))) ; your reserve top 7D + (aset (cg-get g :waste) 0 nil) (aset (cg-get g :found) 0 nil) + (dotimes (k 8) (aset (cg-get g :houses) k (list '(1 . 10)))) ; nowhere on a house, none empty + (cg-put g :turn 1) + (let ((cg-crap--recording nil)) (cg-crap--ai-play g)) + (should (null (cg-crap--reserve g 1))) + (should (member '(2 . 5) (cg-crap--reserve g 0))))) ; loaded onto you + +(ert-deftest cgt-svg-logo-smoke () + (dolist (choice '(modern classic gnu splash drawn none nonexistent)) + (let ((cg-svg-emacs-logo choice) (svg (svg-create 200 120))) + (cg-svg-draw-logo svg 100 60 1.0) + (should (imagep (svg-image svg)))))) + +(ert-deftest cgt-svg-card-back-smoke () + (dolist (b '(dots rings solid lattice waves diamond + emacs emacs-classic gnu splash random)) + (let ((cg-svg-card-back b) (svg (svg-create 80 100))) + (cg-svg-card svg 12 10 :down t) + (should (imagep (svg-image svg))))) + (cg-svg--roll-back) + (should (memq cg-svg--random-back cg-svg--card-backs))) + (ert-deftest cgt-crap-house-run () (let ((g (cg-crap--deal (cg-crapette-game)))) (aset (cg-get g :houses) 0 (list '(2 . 8) '(0 . 7) '(3 . 6))) ; 9D 8S 7H @@ -894,6 +936,65 @@ (should (rassoc '(hand . 0) (get-text-property 0 'cg-regions (cg-fish--svg g)))))) +(ert-deftest cgt-eights-svg-smoke () + (let ((g (cg-eights-game))) + (cg-put g :nplayers 3) (cg-put g :scores (make-vector 3 0)) + (cg-eights--deal g) (cg-put g :cursor 0) (cg-put g :message "x") + (should (stringp (cg-eights--render-text g))) + (let ((regs (get-text-property 0 'cg-regions (cg-eights--board-svg g)))) + (should (rassoc '(hand . 0) regs)) + (should (cl-every (lambda (r) (= 4 (length (car r)))) regs))))) + +(ert-deftest cgt-spite-svg-smoke () + (let ((g (cg-spite-game))) + (cg-put g :nplayers 2) (cg-put g :scores (make-vector 2 0)) + (cg-spite--deal g) (cg-put g :cursor 0) (cg-put g :message "x") + (aset (cg-get g :center) 0 (cons 3 (list '(1 . 3)))) + (should (stringp (cg-spite--render-text g))) + (should (rassoc '(hand . 0) + (get-text-property 0 'cg-regions (cg-spite--board-svg g)))))) + +(ert-deftest cgt-om-svg-smoke () + (let ((g (cg-old-maid-game))) + (cg-om--deal g) + (cg-put g :phase 'play) (cg-put g :turn 0) (cg-put g :message "x") + (should (stringp (cg-om--render-text g))) + (should (stringp (cg-om--svg g))))) + +(ert-deftest cgt-crib-svg-smoke () + (let ((g (cg-cribbage-game))) + (cg-put g :scores (make-vector 2 0)) + (cg-crib--deal g) + (cg-put g :cursor 0) (cg-put g :message "x") + (should (stringp (cg-crib--render-text g))) + (let ((regs (get-text-property 0 'cg-regions (cg-crib--svg g)))) + (should (rassoc '(hand . 0) regs)) + (should (cl-every (lambda (r) (= 4 (length (car r)))) regs)))) + (let ((g (cg-cribbage-game))) + (cg-put g :scores (vector 60 90)) + (cg-crib--deal g) + (cg-put g :phase 'play) (cg-put g :starter '(2 . 4)) (cg-put g :total 15) + (cg-put g :play (vector (cg-crib--hand g 0) (cg-crib--hand g 1))) + (cg-put g :seq (list '(0 . 6) '(3 . 8))) (cg-put g :message "x") + (should (stringp (cg-crib--svg g))))) + +(ert-deftest cgt-bridge-svg-smoke () + (let ((g (cg-bridge-game))) + (cg-bridge--deal g) + (cg-put g :cursor 0) (cg-put g :turn 0) (cg-put g :bid-level 1) (cg-put g :bid-strain 0) + (cg-put g :message "x") + (should (stringp (cg-bridge--render-text g))) + (should (rassoc '(hand . 0) + (get-text-property 0 'cg-regions (cg-bridge--svg g))))) + (let ((g (cg-bridge-game))) + (cg-bridge--deal g) + (cg-put g :phase 'play) (cg-put g :turn 0) (cg-put g :cursor 0) + (cg-put g :declarer 0) (cg-put g :dummy 2) (cg-put g :exposed t) (cg-put g :tricks 0) + (cg-put g :contract '(3 . 3)) (cg-put g :doubled nil) + (cg-put g :trick (list (cons 1 (car (cg-bridge--hand g 1))))) + (cg-put g :message "x") + (should (stringp (cg-bridge--svg g))))) + (ert-deftest cgt-pat-golf-deal () (let ((g (cg-pat--deal (cg-golf-game)))) (should (= 35 (length (cg-get g :cards)))) @@ -1452,3 +1553,46 @@ (should (plist-get rg :help-close)) (should (plist-get rg :help-classic)) (should (plist-get rg :help-quit))))) + + +(ert-deftest cgt-red-suit-boolean () + "Two red suits compare equal by colour (the diamond/heart eq bug)." + (should (eq (cg-red-suit-p 2) (cg-red-suit-p 3))) ; both red -> eq + (should (eq (cg-red-suit-p 0) (cg-red-suit-p 1))) ; both black -> eq + (should-not (eq (cg-red-suit-p 2) (cg-red-suit-p 0))) ; red vs black + ;; a diamond may NOT sit on a heart in a house (both red) + (let ((g (cg-crap--deal (cg-crapette-game)))) + (aset (cg-get g :houses) 0 (list '(3 . 5))) ; 6 of hearts + (should-not (cg-crap--house-accepts g 0 '(2 . 4))) ; 5 of diamonds (both red) + (aset (cg-get g :houses) 0 (list '(0 . 5))) ; 6 of spades (black) + (should (cg-crap--house-accepts g 0 '(2 . 4))))) ; 5 of diamonds ok + +(ert-deftest cgt-ai-level-easy-crapette () + "The easy Crapette AI does not rearrange houses, so a stuck card stays." + (let ((g (cg-crap--deal (cg-crapette-game)))) + (aset (cg-get g :reserve) 1 (list '(2 . 4))) + (aset (cg-get g :waste) 1 nil) (aset (cg-get g :hand) 1 nil) + (aset (cg-get g :reserve) 0 nil) (aset (cg-get g :waste) 0 nil) + (aset (cg-get g :found) 0 nil) + (aset (cg-get g :houses) 0 (list '(0 . 5) '(3 . 5))) + (aset (cg-get g :houses) 1 (list '(1 . 6))) + (dotimes (k 6) (aset (cg-get g :houses) (+ 2 k) (list '(2 . 6)))) + (cg-put g :turn 1) + (let ((cg-ai-level 'easy) (cg-crap--recording nil)) (cg-crap--ai-play g)) + (should (cg-crap--reserve g 1)))) + + +(ert-deftest cgt-treatment-set () + (let ((saved (mapcar (lambda (v) (cons v (symbol-value v))) + (append card-games--svg-card-vars card-games--full-svg-vars))) + (savedt card-games-treatment)) + (unwind-protect + (progn + (card-games-set-treatment 'text) + (should-not cg-sol-svg-cards) (should-not cg-crapette-svg-cards) + (card-games-set-treatment 'full) + (should cg-sol-svg-cards) (should cg-bid-svg-ui) (should cg-gaps-svg-ui) + (card-games-set-treatment 'svg) + (should cg-sol-svg-cards) (should-not cg-bid-svg-ui)) + (dolist (pr saved) (set (car pr) (cdr pr))) + (setq card-games-treatment savedt))))