Replace bid-net advice with a dispatch seam; MELPA review cleanups

Address riscy's first-pass review of MELPA PR #10147.

* card-games-bid-ui.el (card-games-bid-submit): New generic; the four
player commands validate once and submit through it.
(card-games-bid-after-refresh-hook): New hook run after refresh.
(card-games-bid-mode-map): Bind "s" here, not from bid-net at load.
* card-games-bid-net.el (card-games-bid-client-game): New subclass
whose card-games-bid-submit method forwards intents to the host.
Remove all six top-level advice-adds and the top-level define-key.
* card-games-bid.el (card-games-bid-inhibit-prompts): New variable
replacing the nominate-suit advice.
* card-games-core.el, card-games-gaps.el, card-games-crapette.el,
card-games-solitaire.el, card-games-trick-ext.el, card-games-bid-ui.el:
Docstring widths, checkdoc disambiguations, sharp-quoted defalias,
derived-mode-p, require NOERROR.
* test/card-games-tests.el: Six new tests pinning the no-advice
contract and the dispatch seam.
* Bump to 1.0.92; NEWS entry; regenerate README.md.

Assisted-by: Claude:claude-fable-5
This commit is contained in:
Corwin Brust 2026-08-16 09:51:47 -05:00
parent ff7a9f6e49
commit 1a5d23d39e
33 changed files with 359 additions and 186 deletions

95
BE16.png Normal file
View file

@ -0,0 +1,95 @@
<!doctype html>
<html lang="en">
<head>
<meta name="referrer" content="unsafe-url">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Potential Threat Detected</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="./browserconfig.xml">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" href="./css/theme-xdns-security.min.css" inline>
<script type="text/javascript" src="js/jquery/jquery.js"></script>
<script type="text/javascript" src="js/class/class.min.js"></script>
<script type="text/javascript" src="js/jquery-encoder/jquery.jquery-encoder.min.js"></script>
<script type="text/javascript" src="js/dom-purify/purify.min.js"></script>
<script type="text/javascript" src="js/warn.js"></script>
</head>
<body onload="render()">
<header class="header">
<img src="./img/icon_enhanced-security-no-threats.svg" height="70" alt="" inline>
</header>
<div class="wrapper">
<h1>This site could be risky</h1>
<hr noshade>
<div class="section_info">
<p>Advanced Security blocked access to </p>
<p id="site_en"></p>
</div>
<div class="subheader">
<p>This site might compromise your device or contain high-risk content.</p>
<p>To avoid these risks, we recommend avoiding this site.</p>
</div>
<div class="buttons_en">
<a href="#" class="access_link" id="unsafe" rel="noopener noreferrer">Visit anyway</a>
</div>
</div>
<div class="wrapper">
<h1>Ce site pourrait compromettre la sécurité</h1>
<hr noshade>
<div class="section_info">
<p>La fonction de Sécurité avancée a bloqué l’accès à </p>
<p id="site_fr"></p>
</div>
<div class="subheader">
<p>Pour éviter ces risques, nous recommandons d’éviter ce site.</p>
<p>Ce site pourrait compromettre votre appareil ou contenir du contenu présentant un risque élevé.</p>
</div>
<div class="buttons_fr">
<a href="#" class="access_link" id="unsafe" rel="noopener noreferrer">Visitez le site malgré tout</a>
</div>
</div>
<div class="wrapper">
<h1>Este sitio podría ser arriesgado</h1>
<hr noshade>
<div class="section_info">
<p>Advanced Security bloqueó el acceso a</p>
<p id="site_es"></p>
</div>
<div class="subheader">
<p>Este sitio puede poner en peligro a tu dispositivo o tener contenido de alto riesgo.</p>
<p>Para evitar estos riesgos, recomendamos evitar este sitio.</p>
</div>
<div class="buttons_es">
<a href="#" class="access_link" id="unsafe" rel="noopener noreferrer">Visitar de todos modos</a>
</div>
</div>
<div class="wrapper">
<h1>Questo sito potrebbe essere pericoloso</h1>
<hr noshade>
<div class="section_info">
<p>Wifi Sicuro ha bloccato l'accesso a </p>
<p id="site_it"></p>
</div>
<div class="subheader">
<p>Questo sito potrebbe compromettere il dispositivo o includere contenuti ad alto rischio.</p>
<p>Per evitare questi rischi, consigliamo di non visitare questo sito.</p>
</div>
<div class="buttons_it">
<a href="#" class="access_link" id="unsafe" rel="noopener noreferrer">Visita comunque</a>
</div>
</div>
</body>
</html>

View file

@ -1,7 +1,7 @@
# Makefile for card-games -- byte-compile, test, and package.
EMACS ?= emacs
PKG = card-games
VERSION = 1.0.91
VERSION = 1.0.92
# Source files in dependency order (card-games-core first).
EL = card-games-core.el card-games-svg.el card-games-render.el card-games-net.el card-games-bid.el card-games-gaps.el card-games-bid-ui.el card-games-bid-net.el card-games-solitaire.el card-games-trick.el card-games-eights.el card-games-patience.el card-games-president.el card-games-rummy.el card-games-rum500.el card-games-handfoot.el card-games-match.el card-games-cribbage.el card-games-scopa.el card-games-trick-ext.el card-games-spite.el card-games-bridge.el card-games-crapette.el card-games.el
ELC = $(EL:.el=.elc)

19
NEWS
View file

@ -1,6 +1,25 @@
card-games NEWS -- user-visible changes
========================================
* Version 1.0.92 (pretest3: MELPA review)
** Packaging and internals
- The networked 500 client no longer patches game commands with
advice. Player commands submit their action through the new
~card-games-bid-submit~ generic; a joining Emacs plays a
~card-games-bid-client-game~, whose method forwards each intent to
the host. Behaviour is unchanged, and the client now shares the
solo game's turn and legality checks exactly.
- The host broadcasts from the new ~card-games-bid-after-refresh-hook~
instead of advising the refresh loop, and prompts that cannot reach
a remote player are suppressed with ~card-games-bid-inhibit-prompts~.
- The lobby start key ~s~ is defined in ~card-games-bid-mode-map~
itself rather than pushed in when the network layer loads.
- Byte-compile and checkdoc are warning-clean across the package
(docstring widths, ambiguous doc references, ~derived-mode-p~, a
sharp-quoted ~defalias~, and ~require~'s NOERROR argument), from
MELPA review feedback (thanks riscy).
* Version 1.0.91 (pretest2: playtest)
** Documentation

View file

@ -175,9 +175,9 @@ with its command. From the menu you can also switch the card treatment
## From the package tarball
make package # builds card-games-1.0.91.tar
make package # builds card-games-1.0.92.tar
Then in Emacs: `M-x package-install-file RET card-games-1.0.91.tar`.
Then in Emacs: `M-x package-install-file RET card-games-1.0.92.tar`.
## With `use-package`
@ -246,7 +246,7 @@ mouse: click cards, board slots, buttons, and the slider.
# Testing
This is a 1.0.91 pre-test snapshot. To try it:
This is a 1.0.92 pre-test snapshot. To try it:
1. `make compile && make test` &ndash; should be warning-free and all green.
2. `M-x card-games` opens the menu, or jump straight in, e.g.

View file

@ -151,9 +151,9 @@ with its command. From the menu you can also switch the card treatment
* Install
** From the package tarball
#+begin_src
make package # builds card-games-1.0.91.tar
make package # builds card-games-1.0.92.tar
#+end_src
Then in Emacs: ~M-x package-install-file RET card-games-1.0.91.tar~.
Then in Emacs: ~M-x package-install-file RET card-games-1.0.92.tar~.
** With ~use-package~
Once the package is on your ~load-path~ (installed from the tarball or an
@ -213,7 +213,7 @@ mouse: click cards, board slots, buttons, and the slider.
[[file:doc/images/hearts.png]]
* Testing
This is a 1.0.91 pre-test snapshot. To try it:
This is a 1.0.92 pre-test snapshot. To try it:
1. ~make compile && make test~ -- should be warning-free and all green.
2. ~M-x card-games~ opens the menu, or jump straight in, e.g.
~M-x card-games-klondike~, ~M-x card-games-bid~ (500), ~M-x card-games-gin~, ~M-x card-games-handfoot~.

View file

@ -0,0 +1,33 @@
note we can query here; something wrong with blow code.bru.st push auth in the command?
On branch master
Your branch is up to date with 'upstream/master'.
You are in the middle of an am session.
(fix conflicts and then run "git am --continue")
(use "git am --skip" to skip this patch)
(use "git am --abort" to restore the original branch)
Untracked files:
(use "git add <file>..." to include in what will be committed)
BE16.png
avalon-github-vendoring-setup-shape-queries-Claude.log
nothing added to commit but untracked files present (use "git add" to track)
git version 2.54.0
winupdater.recentlyseenversion=2.25.0.windows.1
user.email=corwin@bru.st
user.name=Corwin Brust
github.user=mplscorwin
transfer.fsckobjects=true
gui.recentrepo=G:/git/emacs-30-patches
git@github.com: Permission denied (publickey).
gh-exit:255
Permission denied, please try again.
brust-exit:0
origin git@github.com:mplscorwin/melpa-cg.git (fetch)
origin git@github.com:mplscorwin/melpa-cg.git (push)
02288397 Add recipe for nu-ts-mode (#10045)
25dcef28 Add recipe for promptu (#10139)
bfd86a93 Update recipe for sol-mode (#10134)
## add-card-games
?? recipes/card-games

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -59,11 +59,6 @@ East, so it is chance, not arrival order, that decides who partners whom."
(defvar card-games-bid--net-seat 0
"This player's absolute seat in a live game (the host is always 0).")
(defvar card-games-bid--applying-remote nil
"Bound non-nil while the host applies a remote player's move.
While set, prompts that would block the host (such as nominating a suit
for a Joker lead) fall back to an automatic choice.")
;;;; Per-seat state filter (host -> client)
(defun card-games-bid--rot (x seat)
@ -176,26 +171,14 @@ non-nil when the move was legal and applied, so the host broadcasts."
(card-games-bid--hand game seat)
(card-games-get game :led)
(card-games-bid-trump (card-games-get game :contract)))))
(let ((card-games-bid--applying-remote t)) (card-games-bid--play game seat card))
(let ((card-games-bid-inhibit-prompts t))
(card-games-bid--play game seat card))
(setq ok t))))
(when ok
(let ((card-games-bid--applying-remote t)) (card-games-bid--run game))
(let ((card-games-bid-inhibit-prompts t)) (card-games-bid--run game))
(card-games-bid--net-host-refresh))
ok))
(defun card-games-bid--net-nominate-advice (orig game seat)
"Around advice for `card-games-bid--nominate-suit'.
While the host applies a remote move (ORIG GAME SEAT), pick the longest
suit automatically instead of prompting."
(if card-games-bid--applying-remote
(let ((counts (make-vector 4 0)) (best 0))
(dolist (c (card-games-bid--hand game seat))
(unless (card-games-bid-joker-p c) (cl-incf (aref counts (car c)))))
(dotimes (s 4) (when (> (aref counts s) (aref counts best)) (setq best s)))
best)
(funcall orig game seat)))
(advice-add 'card-games-bid--nominate-suit :around #'card-games-bid--net-nominate-advice)
;;;; Host bookkeeping and display
(defun card-games-bid--net-host-refresh ()
@ -204,11 +187,11 @@ suit automatically instead of prompting."
(when (buffer-live-p buf)
(with-current-buffer buf (card-games-bid--redisplay)))))
(defun card-games-bid--net-broadcast-advice (&rest _)
"After advice on `card-games-bid--refresh' that broadcasts when hosting."
(defun card-games-bid--net-broadcast ()
"Broadcast fresh per-seat state to every client when hosting.
Runs from `card-games-bid-after-refresh-hook'."
(when (and (eq card-games-bid--net-role 'host) (card-games-net-hosting-p))
(card-games-net-host-broadcast)))
(advice-add 'card-games-bid--refresh :after #'card-games-bid--net-broadcast-advice)
(defun card-games-bid--net-lobby-display ()
"Show the host's pre-game lobby of seats."
@ -246,7 +229,7 @@ The host keeps South (seat 0)."
(when card-games-bid-shuffle-partners (card-games-bid--net-shuffle-seats))
(setq card-games-bid--human-seats (cl-remove-duplicates card-games-bid--human-seats))
(card-games-bid--deal game 3)
(let ((card-games-bid--applying-remote t)) (card-games-bid--run game))
(let ((card-games-bid-inhibit-prompts t)) (card-games-bid--run game))
(card-games-bid--net-host-refresh)
(card-games-net-host-broadcast)))
@ -281,108 +264,40 @@ The host keeps South (seat 0)."
(goto-char (point-min)))
(card-games-bid--redisplay))))))
;;;; Client move interception
;;;; Client game: intents go to the host
(defun card-games-bid--net-client-bid-advice (orig)
"Around advice on `card-games-bid-make-bid' (ORIG): send the bid, do not apply it."
(if (eq card-games-bid--net-role 'client)
(let ((game card-games-bid--game))
(if (or (not (eq (card-games-get game :phase) 'auction))
(/= (card-games-get game :bidder) 0))
(progn (card-games-put game :message "Not your turn to bid.")
(card-games-bid--redisplay))
(let* ((legal (card-games-bid--legal-bids game))
(completion-ignore-case t)
(choices (append
(mapcar (lambda (b)
(cons (format "%-4s %s (%d)"
(card-games-bid--code b)
(card-games-bid-name b)
(card-games-bid-value b))
b))
legal)
'(("Pass" . pass))))
(pick (completing-read
"Your bid (e.g. 7H, 8NT, NL; or Pass): "
(mapcar #'car choices) nil t))
(sel (cdr (assoc pick choices))))
(card-games-net-send-move (if (eq sel 'pass) '(pass) (list 'bid sel)))
(card-games-put game :message "Bid sent — waiting…")
(card-games-bid--redisplay))))
(funcall orig)))
(advice-add 'card-games-bid-make-bid :around #'card-games-bid--net-client-bid-advice)
(defclass card-games-bid-client-game (card-games-bid-game) ()
"A 500 game whose authoritative state lives at a remote host.
Submitting an action forwards the player's intent over the wire; the
host validates it, applies it to the canonical game, and broadcasts a
per-seat view back (see `card-games-bid-submit').")
(defun card-games-bid--net-client-pass-advice (orig)
"Around advice on `card-games-bid-pass' (ORIG): send a pass, do not apply it."
(if (eq card-games-bid--net-role 'client)
(let ((game card-games-bid--game))
(if (or (not (eq (card-games-get game :phase) 'auction))
(/= (card-games-get game :bidder) 0))
(progn (card-games-put game :message "Not your turn to bid.")
(card-games-bid--redisplay))
(card-games-net-send-move '(pass))
(card-games-put game :message "Pass sent — waiting…")
(card-games-bid--redisplay)))
(funcall orig)))
(advice-add 'card-games-bid-pass :around #'card-games-bid--net-client-pass-advice)
(defun card-games-bid--net-client-select-advice (orig)
"Around advice on `card-games-bid-select' (ORIG): send a play, or mark locally."
(if (eq card-games-bid--net-role 'client)
(let* ((game card-games-bid--game)
(phase (card-games-get game :phase))
(card (card-games-bid--current-card)))
(pcase phase
('kitty
(when (eql (card-games-get game :contractor) 0)
(let ((marks (card-games-get game :marks)))
(card-games-put game :marks (if (member card marks)
(remove card marks)
(cons card marks)))
(cl-defmethod card-games-bid-submit ((game card-games-bid-client-game) action)
"Send ACTION to the host and note in GAME that a reply is pending.
The shared validation has already run in the calling command; the host
remains authoritative and validates again before applying."
(card-games-net-send-move action)
(when (eq (car action) 'discard)
(card-games-put game :marks nil))
(card-games-put game :message
(format "%d of 5 marked for discard."
(length (card-games-get game :marks))))
(card-games-bid--redisplay))))
('play
(cond
((/= (card-games-get game :turn) 0)
(card-games-put game :message "Not your turn.") (card-games-bid--redisplay))
((null card) (card-games-bid--redisplay))
(t (card-games-net-send-move (list 'play card))
(card-games-put game :message "Card sent — waiting…")
(card-games-bid--redisplay))))
(_ (card-games-bid--redisplay))))
(funcall orig)))
(advice-add 'card-games-bid-select :around #'card-games-bid--net-client-select-advice)
(defun card-games-bid--net-client-discard-advice (orig)
"Around advice on `card-games-bid-discard-marked' (ORIG): send the discard intent."
(if (eq card-games-bid--net-role 'client)
(let* ((game card-games-bid--game) (marks (card-games-get game :marks)))
(cond
((not (eq (card-games-get game :phase) 'kitty))
(card-games-put game :message "Nothing to discard now.") (card-games-bid--redisplay))
((/= (length marks) 5)
(card-games-put game :message
(format "Mark exactly 5 (have %d)." (length marks)))
(format "%s sent — waiting…"
(pcase (car action)
('bid "Bid") ('pass "Pass")
('discard "Discard") ('play "Card"))))
(card-games-bid--redisplay))
(t (card-games-net-send-move (cons 'discard marks))
(card-games-put game :marks nil)
(card-games-put game :message "Discard sent — waiting…")
(card-games-bid--redisplay))))
(funcall orig)))
(advice-add 'card-games-bid-discard-marked :around #'card-games-bid--net-client-discard-advice)
;;;; Commands
;;;###autoload
(defun card-games-bid-start-now ()
"Start a hosted game immediately, AI filling any empty seats."
"Start a hosted game immediately, AI filling any empty seats.
Bound to \`s' in `card-games-bid-mode-map'; outside a hosted lobby it
only explains itself."
(interactive)
(if (and (eq card-games-bid--net-role 'host)
(eq (card-games-get card-games-bid--game :phase) 'lobby))
(card-games-bid--net-start)
(message "Not hosting a lobby.")))
(define-key card-games-bid-mode-map "s" #'card-games-bid-start-now)
;;;###autoload
(defun card-games-bid-host (port)
@ -400,6 +315,7 @@ The host keeps South (seat 0)."
(card-games-net-host-start card-games-bid--game port)
(setf (card-games-net-host-next-seat card-games-net--host) 1)
(add-hook 'card-games-net-connect-functions #'card-games-bid--net-on-connect)
(add-hook 'card-games-bid-after-refresh-hook #'card-games-bid--net-broadcast)
(card-games-bid--net-lobby-display))
(switch-to-buffer buf)
(message "Hosting 500 on port %d — waiting for players (press s to start)."
@ -414,7 +330,7 @@ The host keeps South (seat 0)."
(let ((buf (get-buffer-create "*500 Bid*")))
(with-current-buffer buf
(card-games-bid-mode)
(setq card-games-bid--game (make-instance 'card-games-bid-game)
(setq card-games-bid--game (make-instance 'card-games-bid-client-game)
card-games-bid--net-role 'client
card-games-bid--human-seats '(0))
(card-games-put card-games-bid--game :phase 'lobby)

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -333,7 +333,8 @@ Folds the controls into the single action-button row (see
;;;; Interaction
(defvar-local card-games-bid--game nil "The `card-games-bid-game' in the current buffer.")
(defvar-local card-games-bid--game nil
"The 500 game object played in the current buffer.")
(defun card-games-bid--mode-line (game)
"Return a mode-line status string for GAME."
@ -430,8 +431,14 @@ The treatment is chosen by `card-games-bid--treatment' and dispatched with
(card-games-renderer-draw renderer game)
(goto-char (point-min))))
(defvar card-games-bid-after-refresh-hook nil
"Normal hook run after `card-games-bid--refresh' settles the table.
The network layer adds to it while hosting, broadcasting each settled
state to the connected clients.")
(defun card-games-bid--refresh ()
"Advance AI to the next human action, animating each turn if enabled."
"Advance AI to the next human action, animating each turn if enabled.
Runs `card-games-bid-after-refresh-hook' once the table has settled."
(let ((game card-games-bid--game))
(if (or (not card-games-bid-animate) (<= card-games-bid-ai-delay 0))
(progn (card-games-bid--run game) (card-games-bid--redisplay))
@ -447,7 +454,8 @@ The treatment is chosen by `card-games-bid--treatment' and dispatched with
card-games-bid-ai-delay))
t)))))
(card-games-bid--redisplay))
(card-games-bid--announce game)))
(card-games-bid--announce game)
(run-hooks 'card-games-bid-after-refresh-hook)))
(defun card-games-bid-left ()
"Move the hand cursor left."
@ -466,6 +474,24 @@ The treatment is chosen by `card-games-bid--treatment' and dispatched with
"Return the card under the hand cursor."
(nth (card-games-get card-games-bid--game :cursor) (card-games-get card-games-bid--game :sorted-hand)))
(cl-defgeneric card-games-bid-submit (game action)
"Submit ACTION for the South player of GAME.
ACTION is (bid BID), (pass), (discard CARD...) or (play CARD), already
validated by the calling command. The base method applies it to the
local game; `card-games-bid-client-game' (see `card-games-bid-net')
overrides this to forward the intent to the hosting Emacs instead.")
(cl-defmethod card-games-bid-submit ((game card-games-bid-game) action)
"Apply ACTION for seat 0 directly to the local GAME, then refresh."
(pcase action
(`(bid ,bid) (card-games-bid--auction-act game 0 bid))
(`(pass) (card-games-bid--auction-act game 0 nil))
(`(discard . ,cards)
(card-games-bid--discard game (card-games-get game :contractor) cards)
(card-games-put game :marks nil))
(`(play ,card) (card-games-bid--play game 0 card)))
(card-games-bid--refresh))
(defun card-games-bid-select ()
"Play (in play phase) or mark/unmark (in kitty phase) the current card."
(interactive)
@ -491,16 +517,19 @@ The treatment is chosen by `card-games-bid--treatment' and dispatched with
(format "%d of 5 marked for discard." np))))))
(card-games-bid--redisplay))))
('play
(if (/= (card-games-get game :turn) 0)
(progn (card-games-put game :message "Not your turn.") (card-games-bid--redisplay))
(let ((legal (card-games-bid-legal-cards (card-games-bid--hand game 0)
(card-games-get game :led)
(card-games-bid-trump (card-games-get game :contract)))))
(if (not (member card legal))
(progn (card-games-put game :message "Illegal — you must follow suit.")
(cond
((/= (card-games-get game :turn) 0)
(card-games-put game :message "Not your turn.")
(card-games-bid--redisplay))
(card-games-bid--play game 0 card)
(card-games-bid--refresh)))))
((null card) (card-games-bid--redisplay))
((not (member card (card-games-bid-legal-cards
(card-games-bid--hand game 0)
(card-games-get game :led)
(card-games-bid-trump
(card-games-get game :contract)))))
(card-games-put game :message "Illegal — you must follow suit.")
(card-games-bid--redisplay))
(t (card-games-bid-submit game (list 'play card)))))
(_ (card-games-bid--redisplay)))))
(defun card-games-bid-discard-marked ()
@ -514,9 +543,7 @@ The treatment is chosen by `card-games-bid--treatment' and dispatched with
((/= (length marks) 5)
(card-games-put game :message (format "Mark exactly 5 (have %d)." (length marks)))
(card-games-bid--redisplay))
(t (card-games-bid--discard game (card-games-get game :contractor) marks)
(card-games-put game :marks nil)
(card-games-bid--refresh)))))
(t (card-games-bid-submit game (cons 'discard marks))))))
(defun card-games-bid--code (bid)
"Return a short ASCII code for BID, e.g. \"7H\", \"8NT\", \"NL\"."
@ -547,8 +574,8 @@ Type a short code such as 7H, 8NT, NL (case-insensitive)."
(pick (completing-read "Your bid (e.g. 7H, 8NT, NL; or Pass): "
(mapcar #'car choices) nil t))
(sel (cdr (assoc pick choices))))
(card-games-bid--auction-act game 0 (if (eq sel 'pass) nil sel))
(card-games-bid--refresh)))))
(card-games-bid-submit game
(if (eq sel 'pass) '(pass) (list 'bid sel)))))))
(defun card-games-bid-pass ()
"Pass during the auction."
@ -558,8 +585,7 @@ Type a short code such as 7H, 8NT, NL (case-insensitive)."
(/= (card-games-get game :bidder) 0))
(progn (card-games-put game :message "Not your turn to bid.")
(card-games-bid--redisplay))
(card-games-bid--auction-act game 0 nil)
(card-games-bid--refresh))))
(card-games-bid-submit game '(pass)))))
(defun card-games-bid-new ()
"Advance to the next hand, or start a fresh game once one is over.
@ -627,6 +653,8 @@ must be played out (unlike the solitaire games)."
(interactive)
(card-games-bid--redisplay))
(declare-function card-games-bid-start-now "card-games-bid-net" ())
(defvar card-games-bid-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "<left>") #'card-games-bid-left)
@ -637,6 +665,7 @@ must be played out (unlike the solitaire games)."
(define-key map "x" #'card-games-bid-discard-marked)
(define-key map "g" #'card-games-bid-redraw)
(define-key map "n" #'card-games-bid-new)
(define-key map "s" #'card-games-bid-start-now)
(define-key map "?" #'card-games-bid-help)
(define-key map "+" #'card-games-bid-zoom-in)
(define-key map "=" #'card-games-bid-zoom-in)
@ -727,7 +756,8 @@ per-deal, so the hand always fits the table."
(apply #'svg-text svg str a)))
(defun card-games-bid--ui-label (svg str x y &optional size)
"Draw STR as an all-caps, letter-spaced section label on SVG (font SIZE, default 10)."
"Draw STR at X,Y as an all-caps, letter-spaced label on SVG.
SIZE is the font size in pixels, defaulting to 10."
(svg-text svg (upcase str) :x (round x) :y (round y) :font-size (round (or size 10))
:fill "#8fc79b" :text-anchor "start" :font-family card-games-svg-font-family
:font-weight "bold" :letter-spacing "2"))
@ -857,7 +887,8 @@ nullo bids share the bottom row."
(list (+ gx (* col (+ cw g))) (+ gy (* row (+ ch g))) cw ch))))
(defun card-games-bid--grid-pass-cell (gx gy cw ch g)
"Return (X Y W H) for the double-width Pass button, from GX GY CW CH G (cols 3-4)."
"Return (X Y W H) for the double-width Pass button (columns 3-4).
GX GY are the grid origin, CW CH the cell size, and G the gutter."
(list (+ gx (* 3 (+ cw g))) (+ gy (* 5 (+ ch g))) (+ (* 2 cw) g) ch))
(defun card-games-bid--draw-left-panel (svg game h lpw fs ccy)
@ -1160,7 +1191,7 @@ When `card-games-bid-svg-fill', size the canvas to fill the window."
(defun card-games-bid--fit (&rest _)
"Re-render the SVG-UI to fit the window after a configuration change."
(when (and card-games-bid--game card-games-bid-svg-ui card-games-bid-svg-fill
(eq major-mode 'card-games-bid-mode))
(derived-mode-p 'card-games-bid-mode))
(let ((win (get-buffer-window (current-buffer))))
(when win
(let ((sz (cons (window-body-width win t) (window-body-height win t))))
@ -1196,7 +1227,8 @@ Elsewhere, fall back to normal buffer scrolling."
((or 'wheel-up 'mouse-4) (card-games-bid-log-up))
((or 'wheel-down 'mouse-5) (card-games-bid-log-down))))))
(unless handled
(ignore-errors (require 'mwheel) (mwheel-scroll event)))))
(when (require 'mwheel nil t)
(ignore-errors (mwheel-scroll event))))))
(defun card-games-bid--region-bid (px py rg)
"Return the bid at PX,PY within REGIONS RG, or nil."

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -217,6 +217,12 @@ Trumps lead (strongest first), then each side suit runs high to low."
(defvar card-games-bid--human-seats '(0)
"List of seats controlled by a human player. South is seat 0.")
(defvar card-games-bid-inhibit-prompts nil
"When non-nil, choices that would prompt fall back to the AI's pick.
The network layer binds this while the host applies a remote player's
move: the seat belongs to a human, but one at another Emacs, so a
local minibuffer prompt could never reach them.")
(defconst card-games-bid-seat-names ["South" "West" "North" "East"]
"Seat labels; partners sit opposite (0/2 and 1/3).")
@ -412,9 +418,12 @@ Trumps lead (strongest first), then each side suit runs high to low."
(card-games-put game :turn (card-games-bid--next-seat game seat)))))
(defun card-games-bid--nominate-suit (game seat)
"Choose the suit GAME SEAT nominates when the Joker leads under no-trump."
"Choose the suit GAME SEAT nominates when the Joker leads under no-trump.
A local human is prompted; the AI, or a human seat while
`card-games-bid-inhibit-prompts' is non-nil, takes the longest suit."
(let ((hand (card-games-bid--hand game seat)))
(if (card-games-bid--human-p seat)
(if (and (card-games-bid--human-p seat)
(not card-games-bid-inhibit-prompts))
(let ((ch (read-char-choice
"Joker leads — nominate a suit [s]pades [c]lubs [d]iamonds [h]earts: "
'(?s ?c ?d ?h))))

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -165,9 +165,9 @@ for their own actions and delegate the rest with `cl-call-next-method'."
(_ nil)))
(defvar card-games-renderers nil
"Alist mapping a treatment name (a symbol) to a `card-games-renderer' subclass.
Populate it with `card-games-register-renderer' and look entries up with
`card-games-make-renderer'.")
"Alist mapping a treatment name (a symbol) to a renderer subclass.
Populate it with `card-games-register-renderer' and look entries up
with `card-games-make-renderer'.")
(defun card-games-register-renderer (name class)
"Register renderer CLASS (an EIEIO class) under the treatment NAME."
@ -291,7 +291,7 @@ size slider and `text-scale-increase' enlarge the cards."
(max 0.3 (min 4.0 (* card-games-card-scale (expt 1.15 amt))))))
(defvar-local card-games-current-game nil
"The `card-games-game' shown in the current buffer (for shared mouse/zoom).")
"The game object shown in the current buffer (for shared mouse/zoom).")
(defvar-local card-games-redisplay-function #'ignore
"Buffer-local function that redraws the current game's buffer.")

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -88,7 +88,7 @@ Set to nil to force the plain-text board everywhere."
"Two-player Russian Bank (Crapette): you (South) versus one AI opponent.")
(defvar-local card-games-crap--game nil
"The `card-games-crapette-game' played in the current buffer.")
"The Crapette game object played in the current buffer.")
(defvar card-games-crap--recording t
"When nil, `card-games-crap--snapshot' does not record (used during the AI turn).")

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -106,7 +106,7 @@ Subclasses set the head rank and build direction by overriding
(cl-defmethod card-games-gaps--step ((_ card-games-acre-game)) "Hell's Half-Acre builds down, -1 per column." -1)
(cl-defmethod card-games-gaps--vname ((_ card-games-acre-game)) "Return Hell's Half-Acre's display name." "Hell's Half-Acre")
(defalias 'card-games-gaps--shuffle 'card-games-shuffle)
(defalias 'card-games-gaps--shuffle #'card-games-shuffle)
(defun card-games-gaps--full-deck ()
"Return the 48 playable cards (Two..King in every suit)."
@ -387,7 +387,7 @@ Only used when `card-games-gaps-svg-ui' is enabled."
;;;; Interaction
(defvar-local card-games-gaps--game nil
"The `card-games-gaps-game' object played in the current buffer.")
"The gaps-family game object played in the current buffer.")
(defun card-games-gaps--goto-cell (r c)
"Move point onto the rendered cell at row R column C, if present."
@ -782,7 +782,7 @@ The board scales to fill the area beside a proportional left panel."
(defun card-games-gaps--fit (&rest _)
"Re-render the full-SVG gaps UI to fit the window after a config change."
(when (and card-games-gaps--game card-games-gaps-svg-ui card-games-gaps-svg-fill
(eq major-mode 'card-games-gaps-mode))
(derived-mode-p 'card-games-gaps-mode))
(let ((win (get-buffer-window (current-buffer))))
(when win
(let ((sz (cons (window-body-width win t) (window-body-height win t))))

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -1,5 +1,5 @@
;;; card-games-pkg.el --- Package metadata -*- no-byte-compile: t; -*-
(define-package "card-games" "1.0.91"
(define-package "card-games" "1.0.92"
"Play card games (console UNICODE and graphical SVG)."
'((emacs "26.1"))
:keywords '("games")

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -967,8 +967,9 @@ DISPLAY is a propertized one-image string; REGIONS is a click map of
(redeal :initform t) (build :initform 'alt) (run-rule :initform 'alt)
(empty-rule :initform 'any) (base :initform 0) (wrap :initform nil)
(vname :initform "Russian Bank"))
"Russian Bank patience: eight houses down by alternating colour, four
foundations up by suit from the Ace, and a thirteen-card reserve.")
"Russian Bank patience.
Eight houses build down by alternating colour, four foundations build
up by suit from the Ace, and a thirteen-card reserve feeds the game.")
(cl-defmethod card-games-sol--deal ((game card-games-russian-bank-game))
"Deal GAME's Russian Bank layout: reserve, eight houses, and a stock."

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el
@ -218,7 +218,8 @@ POWERFN, LEDFN rank cards; VALUEFN gives a card's point worth."
(t t)))))))
(cl-defmethod card-games-trick--play ((game card-games-pitch-game) seat card)
"In GAME, set trump from the pitcher's first lead (SEAT plays CARD), then play on."
"In GAME, set trump from the pitcher's first lead, then play on.
SEAT plays CARD as usual once trump is fixed."
(when (and (null (oref game trump)) (null (card-games-get game :trick)))
(oset game trump (car card))
(card-games-put game :message

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91
;; Version: 1.0.92
;; Package-Requires: ((emacs "26.1"))
;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el

View file

@ -1,3 +1,3 @@
@set VERSION 1.0.91
@set VERSION 1.0.92
@set UPDATED 1 July 2026
@set YEAR 2026

View file

@ -1837,3 +1837,70 @@ left a 15-card hand in play."
(should card-games-sol-svg-cards) (should-not card-games-bid-svg-ui))
(dolist (pr saved) (set (car pr) (cdr pr)))
(setq card-games-treatment savedt))))
;;;; Live 500: dispatch seam replaces advice (MELPA review, PR #10147)
(defun cgt--advised-p (symbol)
"Return non-nil when SYMBOL carries any advice."
(let (found)
(advice-mapc (lambda (&rest _) (setq found t)) symbol)
found))
(ert-deftest cgt-bid-net-no-advice ()
"Loading the net layer must not install advice on bid functions."
(require 'card-games-bid-net)
(dolist (sym '(card-games-bid--nominate-suit card-games-bid--refresh
card-games-bid-make-bid card-games-bid-pass
card-games-bid-select card-games-bid-discard-marked))
(should-not (cgt--advised-p sym))))
(ert-deftest cgt-bid-submit-local ()
"`card-games-bid-submit' applies an action locally on a base game."
(let ((game (make-instance 'card-games-bid-game)) calls)
(cl-letf (((symbol-function 'card-games-bid--auction-act)
(lambda (_g seat bid) (push (list 'act seat bid) calls)))
((symbol-function 'card-games-bid--refresh)
(lambda () (push 'refresh calls))))
(card-games-bid-submit game '(pass)))
(should (equal (reverse calls) '((act 0 nil) refresh)))))
(ert-deftest cgt-bid-submit-client ()
"`card-games-bid-submit' on a client game sends and never applies."
(require 'card-games-bid-net)
(let ((game (make-instance 'card-games-bid-client-game)) sent applied)
(cl-letf (((symbol-function 'card-games-net-send-move)
(lambda (move) (push move sent)))
((symbol-function 'card-games-bid--auction-act)
(lambda (&rest _) (setq applied t)))
((symbol-function 'card-games-bid--redisplay) #'ignore))
(card-games-bid-submit game '(pass)))
(should (equal sent '((pass))))
(should-not applied)
(should (equal (card-games-get game :message) "Pass sent — waiting…"))))
(ert-deftest cgt-bid-nominate-inhibit ()
"Inhibited prompts fall back to the AI pick for a human seat."
(require 'card-games-bid-net)
(let ((game (make-instance 'card-games-bid-game)))
(card-games-put game :hands (vector '((2 . 5) (2 . 6) (0 . 14)) nil nil nil))
(cl-letf (((symbol-function 'read-char-choice)
(lambda (&rest _) (error "Prompted despite inhibit"))))
(let ((card-games-bid-inhibit-prompts t)
(card-games-bid--human-seats '(0)))
(should (= (card-games-bid--nominate-suit game 0) 2))))))
(ert-deftest cgt-bid-after-refresh-hook ()
"`card-games-bid--refresh' runs `card-games-bid-after-refresh-hook'."
(let ((game (make-instance 'card-games-bid-game)) ran)
(cl-letf (((symbol-function 'card-games-bid--run) #'ignore)
((symbol-function 'card-games-bid--redisplay) #'ignore)
((symbol-function 'card-games-bid--announce) #'ignore))
(let ((card-games-bid--game game)
(card-games-bid-animate nil)
(card-games-bid-after-refresh-hook (list (lambda () (setq ran t)))))
(card-games-bid--refresh)))
(should ran)))
(ert-deftest cgt-bid-start-now-key ()
"The lobby start key lives in the owner keymap, not a load-time patch."
(should (eq (lookup-key card-games-bid-mode-map "s") 'card-games-bid-start-now)))