;;; card-games.el --- Play card games in Emacs (console + SVG) -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Corwin Brust ;; Author: Corwin Brust ;; Maintainer: Corwin Brust ;; Version: 1.0.90 ;; Package-Requires: ((emacs "26.1")) ;; Keywords: games ;; URL: https://code.bru.st/corwin/card-game.el ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Card games for Emacs, rendered as UNICODE text in a terminal and as ;; SVG cards on a graphical display. This file is the umbrella: it ;; pulls in the individual games and offers a chooser. ;; ;; Run `M-x card-game' for a menu, or start a game directly: ;; ;; `cg-bid' -- 500, the four-handed partnership trick-taking game, ;; played against three computer opponents. ;; `cg-gaps' -- Gaps / Montana / "Hell's Half-Acre" solitaire. ;; ;; New games register themselves by adding to `card-games-list'. ;;; Code: (require 'cg-core) (require 'cg-render) (require 'cg-net) (require 'cg-gaps) (require 'cg-bid-ui) (require 'cg-bid-net) (require 'cg-solitaire) (require 'cg-trick) (require 'cg-eights) (require 'cg-patience) (require 'cg-president) (require 'cg-rummy) (require 'cg-rum500) (require 'cg-handfoot) (require 'cg-match) (require 'cg-cribbage) (require 'cg-scopa) (require 'cg-trick-ext) (require 'cg-spite) (require 'cg-bridge) (require 'cg-crapette) (defvar card-games-list '(("500 (Bid)" cg-bid "Four-handed partnership trick-taking versus three AI opponents.") ("Gaps (Montana)" cg-montana "Solitaire: a Two anchors each row; build up 2 through King.") ("Hell's Half-Acre" cg-hells-half-acre "Solitaire: a King anchors each row; build down King through 2.") ("Klondike" cg-klondike "Solitaire: the classic; build the foundations up by suit from the Ace.") ("FreeCell" cg-freecell "Solitaire: every card in view, four free cells, a game of skill.") ("Spider" cg-spider "Solitaire: two decks; build down and clear eight same-suit runs.") ("Yukon" cg-yukon "Solitaire: Klondike's layout, all face up; move any buried group.") ("Hearts" cg-hearts "Trick-taking: dodge every heart and the Queen of Spades.") ("Spades" cg-spades "Trick-taking: partnership bidding to 500; spades are always trump.") ("Crazy Eights" cg-eights "Shedding: match the suit or rank; eights are wild.") ("Canfield" cg-canfield "Solitaire: a 13-card reserve and a shifting foundation base rank.") ("Russian Bank" cg-russian-bank "Solitaire: eight houses down by alternating colour; a reserve feeds them.") ("Forty Thieves" cg-forty-thieves "Solitaire: two decks, ten columns, eight foundations, no redeal.") ("Scorpion" cg-scorpion "Solitaire: build down by suit and free four buried King-to-Ace runs.") ("Golf" cg-golf "Solitaire: clear the layout one rank at a time onto the waste.") ("TriPeaks" cg-tripeaks "Solitaire: clear three peaks with Ace-King wrapping chains.") ("Pyramid" cg-pyramid "Solitaire: remove pairs of cards that sum to thirteen.") ("Whist" cg-whist "Trick-taking: fixed trump, no bidding, race past the book of six.") ("Oh Hell" cg-ohhell "Trick-taking: shrinking hands; bid the exact tricks you will take.") ("President" cg-president "Climbing: shed your hand; first out rules, last out scrubs.") ("Gin Rummy" cg-gin "Rummy: form melds, knock with little deadwood; head to head to 100.") ("Rummy" cg-rummy-basic "Rummy: meld your whole hand to the table to go out.") ("Rummy 500" cg-rum500 "Rummy: score the cards you lay down; race past 500.") ("Hand & Foot" cg-handfoot "Rummy: partnership Canasta cousin; build books from hand and foot.") ("Go Fish" cg-go-fish "Matching: ask for ranks and collect books of four.") ("Old Maid" cg-old-maid "Matching: shed pairs and avoid the leftover Queen.") ("Cribbage" cg-cribbage "Pegging and the show: fifteens, pairs, runs, and his nobs to 121.") ("Scopa" cg-scopa "Capturing: take table cards by value; sweep for a scopa.") ("Casino" cg-casino "Capturing: pairs and sums; big and little casino, aces, sweeps.") ("Euchre" cg-euchre "Trick-taking: 24 cards, bowers, order up; partnership to 10.") ("Pitch" cg-pitch "Trick-taking: bid, pitch to set trump, score High-Low-Jack-Game.") ("Briscola" cg-briscola "Trick-taking: fixed trump, no follow; capture the points to 61.") ("Spite & Malice" cg-spite "Climbing patience: race to empty your goal pile; Kings are wild.") ("Bridge" cg-bridge "Trick-taking: the auction, the dummy, and rubber scoring, to 121.") ("Crapette (Russian Bank)" cg-crapette "Two-player Russian Bank versus the computer; empty your reserve, hand, and waste.")) "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))) (quit-window) (call-interactively cmd))) (defvar card-game-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map special-mode-map) (define-key map "n" #'forward-button) (define-key map "p" #'backward-button) (define-key map (kbd "TAB") #'forward-button) (define-key map (kbd "") #'backward-button) map) "Keymap for `card-game-mode'.") (define-derived-mode card-game-mode special-mode "Card-Games" "Major mode for the `card-game' chooser." (setq-local cursor-type cg-cursor-type)) ;;;###autoload (defun card-game () "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) (let ((inhibit-read-only t)) (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" '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 (format "%-26s" (nth 0 g)) 'face 'link 'help-echo (nth 2 g) 'card-game-command (nth 1 g) 'action #'card-game--launch) (insert (propertize (concat " " (nth 2 g) "\n") 'face 'shadow))) (insert "\n"))) (switch-to-buffer buf) (goto-char (point-min)) (forward-button 1))) ;;;###autoload (defalias 'card-games #'card-game "Alias for `card-game'.") (defconst card-games-themes '((classic :felt "#15692f" :theme t) (dark :felt "#23272e" :back "#3b4252" :highlight "#88c0d0" :theme nil) (contrast :felt "#0a0a0a" :back "#000000" :highlight "#ffd400" :theme nil)) "Named table/card colour presets for `card-games-set-theme'.") ;;;###autoload (defun card-games-set-theme (name) "Apply the card-games colour preset NAME (see `card-games-themes')." (interactive (list (intern (completing-read "Card-games theme: " (mapcar (lambda (e) (symbol-name (car e))) card-games-themes) nil t)))) (let ((p (alist-get name card-games-themes))) (unless p (user-error "No such card-games theme: %s" name)) (setq cg-bid-felt-color (plist-get p :felt)) (when (plist-member p :theme) (setq cg-svg-theme-colors (plist-get p :theme))) (when (plist-get p :back) (setq cg-svg-back-color (plist-get p :back))) (when (plist-get p :highlight) (setq cg-svg-highlight-color (plist-get p :highlight))) (message "card-games theme: %s" name))) (provide 'card-games) ;;; card-games.el ends here