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. # Makefile for card-games -- byte-compile, test, and package.
EMACS ?= emacs EMACS ?= emacs
PKG = card-games PKG = card-games
VERSION = 1.0.91 VERSION = 1.0.92
# Source files in dependency order (card-games-core first). # 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 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) ELC = $(EL:.el=.elc)

19
NEWS
View file

@ -1,6 +1,25 @@
card-games NEWS -- user-visible changes 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) * Version 1.0.91 (pretest2: playtest)
** Documentation ** Documentation

View file

@ -175,9 +175,9 @@ with its command. From the menu you can also switch the card treatment
## From the package tarball ## 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` ## With `use-package`
@ -246,7 +246,7 @@ mouse: click cards, board slots, buttons, and the slider.
# Testing # 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. 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. 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 * Install
** From the package tarball ** From the package tarball
#+begin_src #+begin_src
make package # builds card-games-1.0.91.tar make package # builds card-games-1.0.92.tar
#+end_src #+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~ ** With ~use-package~
Once the package is on your ~load-path~ (installed from the tarball or an 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]] [[file:doc/images/hearts.png]]
* Testing * 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. 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. 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~. ~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> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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 (defvar card-games-bid--net-seat 0
"This player's absolute seat in a live game (the host is always 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) ;;;; Per-seat state filter (host -> client)
(defun card-games-bid--rot (x seat) (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-bid--hand game seat)
(card-games-get game :led) (card-games-get game :led)
(card-games-bid-trump (card-games-get game :contract))))) (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)))) (setq ok t))))
(when ok (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)) (card-games-bid--net-host-refresh))
ok)) 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 ;;;; Host bookkeeping and display
(defun card-games-bid--net-host-refresh () (defun card-games-bid--net-host-refresh ()
@ -204,11 +187,11 @@ suit automatically instead of prompting."
(when (buffer-live-p buf) (when (buffer-live-p buf)
(with-current-buffer buf (card-games-bid--redisplay))))) (with-current-buffer buf (card-games-bid--redisplay)))))
(defun card-games-bid--net-broadcast-advice (&rest _) (defun card-games-bid--net-broadcast ()
"After advice on `card-games-bid--refresh' that broadcasts when hosting." "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)) (when (and (eq card-games-bid--net-role 'host) (card-games-net-hosting-p))
(card-games-net-host-broadcast))) (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 () (defun card-games-bid--net-lobby-display ()
"Show the host's pre-game lobby of seats." "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)) (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)) (setq card-games-bid--human-seats (cl-remove-duplicates card-games-bid--human-seats))
(card-games-bid--deal game 3) (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-bid--net-host-refresh)
(card-games-net-host-broadcast))) (card-games-net-host-broadcast)))
@ -281,108 +264,40 @@ The host keeps South (seat 0)."
(goto-char (point-min))) (goto-char (point-min)))
(card-games-bid--redisplay)))))) (card-games-bid--redisplay))))))
;;;; Client move interception ;;;; Client game: intents go to the host
(defun card-games-bid--net-client-bid-advice (orig) (defclass card-games-bid-client-game (card-games-bid-game) ()
"Around advice on `card-games-bid-make-bid' (ORIG): send the bid, do not apply it." "A 500 game whose authoritative state lives at a remote host.
(if (eq card-games-bid--net-role 'client) Submitting an action forwards the player's intent over the wire; the
(let ((game card-games-bid--game)) host validates it, applies it to the canonical game, and broadcasts a
(if (or (not (eq (card-games-get game :phase) 'auction)) per-seat view back (see `card-games-bid-submit').")
(/= (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)
(defun card-games-bid--net-client-pass-advice (orig) (cl-defmethod card-games-bid-submit ((game card-games-bid-client-game) action)
"Around advice on `card-games-bid-pass' (ORIG): send a pass, do not apply it." "Send ACTION to the host and note in GAME that a reply is pending.
(if (eq card-games-bid--net-role 'client) The shared validation has already run in the calling command; the host
(let ((game card-games-bid--game)) remains authoritative and validates again before applying."
(if (or (not (eq (card-games-get game :phase) 'auction)) (card-games-net-send-move action)
(/= (card-games-get game :bidder) 0)) (when (eq (car action) 'discard)
(progn (card-games-put game :message "Not your turn to bid.") (card-games-put game :marks nil))
(card-games-bid--redisplay)) (card-games-put game :message
(card-games-net-send-move '(pass)) (format "%s sent — waiting…"
(card-games-put game :message "Pass sent — waiting…") (pcase (car action)
(card-games-bid--redisplay))) ('bid "Bid") ('pass "Pass")
(funcall orig))) ('discard "Discard") ('play "Card"))))
(advice-add 'card-games-bid-pass :around #'card-games-bid--net-client-pass-advice) (card-games-bid--redisplay))
(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)))
(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)))
(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 ;;;; Commands
;;;###autoload
(defun card-games-bid-start-now () (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) (interactive)
(if (and (eq card-games-bid--net-role 'host) (if (and (eq card-games-bid--net-role 'host)
(eq (card-games-get card-games-bid--game :phase) 'lobby)) (eq (card-games-get card-games-bid--game :phase) 'lobby))
(card-games-bid--net-start) (card-games-bid--net-start)
(message "Not hosting a lobby."))) (message "Not hosting a lobby.")))
(define-key card-games-bid-mode-map "s" #'card-games-bid-start-now)
;;;###autoload ;;;###autoload
(defun card-games-bid-host (port) (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) (card-games-net-host-start card-games-bid--game port)
(setf (card-games-net-host-next-seat card-games-net--host) 1) (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-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)) (card-games-bid--net-lobby-display))
(switch-to-buffer buf) (switch-to-buffer buf)
(message "Hosting 500 on port %d — waiting for players (press s to start)." (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*"))) (let ((buf (get-buffer-create "*500 Bid*")))
(with-current-buffer buf (with-current-buffer buf
(card-games-bid-mode) (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--net-role 'client
card-games-bid--human-seats '(0)) card-games-bid--human-seats '(0))
(card-games-put card-games-bid--game :phase 'lobby) (card-games-put card-games-bid--game :phase 'lobby)

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; URL: https://code.bru.st/corwin/card-game.el
@ -333,7 +333,8 @@ Folds the controls into the single action-button row (see
;;;; Interaction ;;;; 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) (defun card-games-bid--mode-line (game)
"Return a mode-line status string for 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) (card-games-renderer-draw renderer game)
(goto-char (point-min)))) (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 () (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)) (let ((game card-games-bid--game))
(if (or (not card-games-bid-animate) (<= card-games-bid-ai-delay 0)) (if (or (not card-games-bid-animate) (<= card-games-bid-ai-delay 0))
(progn (card-games-bid--run game) (card-games-bid--redisplay)) (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)) card-games-bid-ai-delay))
t))))) t)))))
(card-games-bid--redisplay)) (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 () (defun card-games-bid-left ()
"Move the hand cursor 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." "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))) (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 () (defun card-games-bid-select ()
"Play (in play phase) or mark/unmark (in kitty phase) the current card." "Play (in play phase) or mark/unmark (in kitty phase) the current card."
(interactive) (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)))))) (format "%d of 5 marked for discard." np))))))
(card-games-bid--redisplay)))) (card-games-bid--redisplay))))
('play ('play
(if (/= (card-games-get game :turn) 0) (cond
(progn (card-games-put game :message "Not your turn.") (card-games-bid--redisplay)) ((/= (card-games-get game :turn) 0)
(let ((legal (card-games-bid-legal-cards (card-games-bid--hand game 0) (card-games-put game :message "Not your turn.")
(card-games-get game :led) (card-games-bid--redisplay))
(card-games-bid-trump (card-games-get game :contract))))) ((null card) (card-games-bid--redisplay))
(if (not (member card legal)) ((not (member card (card-games-bid-legal-cards
(progn (card-games-put game :message "Illegal — you must follow suit.") (card-games-bid--hand game 0)
(card-games-bid--redisplay)) (card-games-get game :led)
(card-games-bid--play game 0 card) (card-games-bid-trump
(card-games-bid--refresh))))) (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))))) (_ (card-games-bid--redisplay)))))
(defun card-games-bid-discard-marked () (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) ((/= (length marks) 5)
(card-games-put game :message (format "Mark exactly 5 (have %d)." (length marks))) (card-games-put game :message (format "Mark exactly 5 (have %d)." (length marks)))
(card-games-bid--redisplay)) (card-games-bid--redisplay))
(t (card-games-bid--discard game (card-games-get game :contractor) marks) (t (card-games-bid-submit game (cons 'discard marks))))))
(card-games-put game :marks nil)
(card-games-bid--refresh)))))
(defun card-games-bid--code (bid) (defun card-games-bid--code (bid)
"Return a short ASCII code for BID, e.g. \"7H\", \"8NT\", \"NL\"." "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): " (pick (completing-read "Your bid (e.g. 7H, 8NT, NL; or Pass): "
(mapcar #'car choices) nil t)) (mapcar #'car choices) nil t))
(sel (cdr (assoc pick choices)))) (sel (cdr (assoc pick choices))))
(card-games-bid--auction-act game 0 (if (eq sel 'pass) nil sel)) (card-games-bid-submit game
(card-games-bid--refresh))))) (if (eq sel 'pass) '(pass) (list 'bid sel)))))))
(defun card-games-bid-pass () (defun card-games-bid-pass ()
"Pass during the auction." "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)) (/= (card-games-get game :bidder) 0))
(progn (card-games-put game :message "Not your turn to bid.") (progn (card-games-put game :message "Not your turn to bid.")
(card-games-bid--redisplay)) (card-games-bid--redisplay))
(card-games-bid--auction-act game 0 nil) (card-games-bid-submit game '(pass)))))
(card-games-bid--refresh))))
(defun card-games-bid-new () (defun card-games-bid-new ()
"Advance to the next hand, or start a fresh game once one is over. "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) (interactive)
(card-games-bid--redisplay)) (card-games-bid--redisplay))
(declare-function card-games-bid-start-now "card-games-bid-net" ())
(defvar card-games-bid-mode-map (defvar card-games-bid-mode-map
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(define-key map (kbd "<left>") #'card-games-bid-left) (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 "x" #'card-games-bid-discard-marked)
(define-key map "g" #'card-games-bid-redraw) (define-key map "g" #'card-games-bid-redraw)
(define-key map "n" #'card-games-bid-new) (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-help)
(define-key map "+" #'card-games-bid-zoom-in) (define-key map "+" #'card-games-bid-zoom-in)
(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))) (apply #'svg-text svg str a)))
(defun card-games-bid--ui-label (svg str x y &optional size) (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)) (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 :fill "#8fc79b" :text-anchor "start" :font-family card-games-svg-font-family
:font-weight "bold" :letter-spacing "2")) :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)))) (list (+ gx (* col (+ cw g))) (+ gy (* row (+ ch g))) cw ch))))
(defun card-games-bid--grid-pass-cell (gx gy cw ch g) (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)) (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) (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 _) (defun card-games-bid--fit (&rest _)
"Re-render the SVG-UI to fit the window after a configuration change." "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 (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)))) (let ((win (get-buffer-window (current-buffer))))
(when win (when win
(let ((sz (cons (window-body-width win t) (window-body-height win t)))) (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-up 'mouse-4) (card-games-bid-log-up))
((or 'wheel-down 'mouse-5) (card-games-bid-log-down)))))) ((or 'wheel-down 'mouse-5) (card-games-bid-log-down))))))
(unless handled (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) (defun card-games-bid--region-bid (px py rg)
"Return the bid at PX,PY within REGIONS RG, or nil." "Return the bid at PX,PY within REGIONS RG, or nil."

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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) (defvar card-games-bid--human-seats '(0)
"List of seats controlled by a human player. South is seat 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"] (defconst card-games-bid-seat-names ["South" "West" "North" "East"]
"Seat labels; partners sit opposite (0/2 and 1/3).") "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))))) (card-games-put game :turn (card-games-bid--next-seat game seat)))))
(defun card-games-bid--nominate-suit (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))) (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 (let ((ch (read-char-choice
"Joker leads — nominate a suit [s]pades [c]lubs [d]iamonds [h]earts: " "Joker leads — nominate a suit [s]pades [c]lubs [d]iamonds [h]earts: "
'(?s ?c ?d ?h)))) '(?s ?c ?d ?h))))

View file

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

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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))) (_ nil)))
(defvar card-games-renderers nil (defvar card-games-renderers nil
"Alist mapping a treatment name (a symbol) to a `card-games-renderer' subclass. "Alist mapping a treatment name (a symbol) to a renderer subclass.
Populate it with `card-games-register-renderer' and look entries up with Populate it with `card-games-register-renderer' and look entries up
`card-games-make-renderer'.") with `card-games-make-renderer'.")
(defun card-games-register-renderer (name class) (defun card-games-register-renderer (name class)
"Register renderer CLASS (an EIEIO class) under the treatment NAME." "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)))))) (max 0.3 (min 4.0 (* card-games-card-scale (expt 1.15 amt))))))
(defvar-local card-games-current-game nil (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 (defvar-local card-games-redisplay-function #'ignore
"Buffer-local function that redraws the current game's buffer.") "Buffer-local function that redraws the current game's buffer.")

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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.") "Two-player Russian Bank (Crapette): you (South) versus one AI opponent.")
(defvar-local card-games-crap--game nil (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 (defvar card-games-crap--recording t
"When nil, `card-games-crap--snapshot' does not record (used during the AI turn).") "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> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; URL: https://code.bru.st/corwin/card-game.el

View file

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

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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--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") (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 () (defun card-games-gaps--full-deck ()
"Return the 48 playable cards (Two..King in every suit)." "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 ;;;; Interaction
(defvar-local card-games-gaps--game nil (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) (defun card-games-gaps--goto-cell (r c)
"Move point onto the rendered cell at row R column C, if present." "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 _) (defun card-games-gaps--fit (&rest _)
"Re-render the full-SVG gaps UI to fit the window after a config change." "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 (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)))) (let ((win (get-buffer-window (current-buffer))))
(when win (when win
(let ((sz (cons (window-body-width win t) (window-body-height win t)))) (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> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; URL: https://code.bru.st/corwin/card-game.el

View file

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

View file

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

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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; -*- ;;; 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)." "Play card games (console UNICODE and graphical SVG)."
'((emacs "26.1")) '((emacs "26.1"))
:keywords '("games") :keywords '("games")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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))))))) (t t)))))))
(cl-defmethod card-games-trick--play ((game card-games-pitch-game) seat card) (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))) (when (and (null (oref game trump)) (null (card-games-get game :trick)))
(oset game trump (car card)) (oset game trump (car card))
(card-games-put game :message (card-games-put game :message

View file

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

View file

@ -4,7 +4,7 @@
;; Author: Corwin Brust <corwin@bru.st> ;; Author: Corwin Brust <corwin@bru.st>
;; Maintainer: Corwin Brust <corwin@bru.st> ;; Maintainer: Corwin Brust <corwin@bru.st>
;; Version: 1.0.91 ;; Version: 1.0.92
;; Package-Requires: ((emacs "26.1")) ;; Package-Requires: ((emacs "26.1"))
;; Keywords: games ;; Keywords: games
;; URL: https://code.bru.st/corwin/card-game.el ;; 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 UPDATED 1 July 2026
@set YEAR 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)) (should card-games-sol-svg-cards) (should-not card-games-bid-svg-ui))
(dolist (pr saved) (set (car pr) (cdr pr))) (dolist (pr saved) (set (car pr) (cdr pr)))
(setq card-games-treatment savedt)))) (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)))