From 2ee802f52d8b78c1d2848222b591b0a7026e1492 Mon Sep 17 00:00:00 2001 From: Corwin Brust Date: Wed, 1 Jul 2026 04:50:10 -0500 Subject: [PATCH] Hand & Foot, Scopa, Casino: full SVG board via the shared helper * cg-rummy.el (cg-rummy--board-svg): add :stock-label and a :discard 'none books and your hand/foot. * cg-scopa.el (cg-fish--svg, cg-fish--render-text): board with the table * NEWS: note the new boards. --- NEWS | 3 +++ cg-handfoot.el | 39 ++++++++++++++++++++++++++++++++++++++- cg-rummy.el | 20 ++++++++++++-------- cg-scopa.el | 22 +++++++++++++++++++++- test/card-games-tests.el | 18 ++++++++++++++++++ 5 files changed, 92 insertions(+), 10 deletions(-) diff --git a/NEWS b/NEWS index 778ae5d..c2f0156 100644 --- a/NEWS +++ b/NEWS @@ -43,6 +43,9 @@ the mouse as well as the keyboard. the stock and discard, the melds already down on the table, and your fanned hand with cursor, marks, and lay-off hints; click a card to select it. Honours ~cg-rummy-svg-cards~. + - The same board now covers Hand & Foot (each team's books, and your + hand or foot) and the fishing games Scopa and Casino (the loose + table cards and the deck). ** Rummy-family rules completed - Rummy 500: take a card from anywhere in the discard pile (key ~T~) -- diff --git a/cg-handfoot.el b/cg-handfoot.el index 52f3920..07acc06 100644 --- a/cg-handfoot.el +++ b/cg-handfoot.el @@ -505,8 +505,45 @@ Return non-nil when the team goes down." (defvar-local cg-hf--game nil "The Hand & Foot game in the current buffer.") +(defun cg-hf--svg (game) + "Return an SVG board for the Hand & Foot GAME." + (let* ((scores (cg-get game :scores)) (infos '()) (melds '())) + (dotimes (team (cg-get game :nteams)) + (push (format "Team %d (score %d)%s%s" team (aref scores team) + (if (cg-hf--down-p game team) " down" + (format " needs %d" (cg-hf--min-for-round game))) + (let ((k (length (aref (cg-get game :redthrees) team)))) + (if (> k 0) (format " red3:%d" k) ""))) + infos) + (dolist (bk (cg-hf--books game team)) + (push (cons (format "T%d%s" team + (if (cg-hf--book-complete-p bk) + (if (cg-hf--book-clean-p bk) " clean" " dirty") "")) + bk) + melds))) + (dotimes (s (cg-get game :nplayers)) + (unless (= s 0) + (push (format "%s: %d in hand%s" (aref cg-handfoot--names s) + (length (cg-rummy--hand game s)) + (if (= (aref (cg-get game :stage) s) 1) " (on foot)" "")) + infos))) + (cg-rummy--board-svg + :title (format "Hand & Foot target %d round %d (min %d)" + cg-handfoot-target (1+ (or (cg-get game :round) 0)) + (cg-hf--min-for-round game)) + :infos (nreverse infos) :melds (nreverse melds) + :discard (cg-rummy--top game) :stock (length (cg-get game :stock)) + :hand (cg-rummy--hand game 0) :cursor (cg-get game :cursor) + :marks (cg-get game :marks) :message (cg-get game :message)))) + (cl-defmethod cg-render ((game cg-handfoot-game)) - "Return a propertized depiction of the Hand & Foot GAME." + "Return a depiction of the Hand & Foot GAME: SVG board if graphical, else text." + (if (and cg-rummy-svg-cards (display-graphic-p)) + (cg-hf--svg game) + (cg-hf--render-text game))) + +(defun cg-hf--render-text (game) + "Return a plain-text depiction of the Hand & Foot GAME." (let* ((out '()) (scores (cg-get game :scores)) (hand (cg-rummy--hand game 0)) (cursor (cg-get game :cursor))) (push (format " Hand & Foot target %d round %d (go-down minimum %d)\n\n" diff --git a/cg-rummy.el b/cg-rummy.el index 20f042e..c41450f 100644 --- a/cg-rummy.el +++ b/cg-rummy.el @@ -335,8 +335,11 @@ Keyword ARGS: :title :infos :melds :discard :stock :hand :cursor :marks (raw-melds (plist-get args :melds)) (show-table (not (eq raw-melds 'none))) (melds (and show-table raw-melds)) - (discard (plist-get args :discard)) + (raw-discard (plist-get args :discard)) + (show-discard (not (eq raw-discard 'none))) + (discard (and show-discard raw-discard)) (stock (or (plist-get args :stock) 0)) + (stock-label (or (plist-get args :stock-label) "Stock")) (hand (plist-get args :hand)) (cursor (or (plist-get args :cursor) 0)) (marks (plist-get args :marks)) @@ -383,13 +386,14 @@ Keyword ARGS: :title :infos :melds :discard :stock :hand :cursor :marks (let ((yy (+ y-info 4))) (dolist (line infos) (txt line pad yy 12) (setq yy (+ yy 16)))) (cg-svg-card svg pad y-sd :down (> stock 0) :gap (= stock 0)) - (txt (format "Stock %d" stock) pad (+ y-sd h 13) 11) - (let ((dx (+ pad w gap 24))) - (if discard - (let ((sp (cg-rummy--card-spec discard))) - (cg-svg-card svg dx y-sd :rank (car sp) :suit (cdr sp))) - (cg-svg-card svg dx y-sd :gap t)) - (txt "Discard" dx (+ y-sd h 13) 11)) + (txt (format "%s %d" stock-label stock) pad (+ y-sd h 13) 11) + (when show-discard + (let ((dx (+ pad w gap 24))) + (if discard + (let ((sp (cg-rummy--card-spec discard))) + (cg-svg-card svg dx y-sd :rank (car sp) :suit (cdr sp))) + (cg-svg-card svg dx y-sd :gap t)) + (txt "Discard" dx (+ y-sd h 13) 11))) (when show-table (txt "Table" pad (- y-melds 4) 11) (if (null melds) diff --git a/cg-scopa.el b/cg-scopa.el index 13770fc..17d8a7c 100644 --- a/cg-scopa.el +++ b/cg-scopa.el @@ -219,8 +219,28 @@ Only subsets of two or more cards are considered. Return nil if none." (defvar-local cg-fish--game nil "The fishing game in the current buffer.") +(defun cg-fish--svg (game) + "Return an SVG board for the fishing GAME." + (cg-rummy--board-svg + :title (format "%s (to %d)" (oref game vname) (oref game target)) + :infos (list (format "Computer: %d cards captured %d (score %d)" + (length (cg-fish--hand game 1)) (length (cg-fish--captured game 1)) + (aref (cg-get game :scores) 1)) + (format "Your captured: %d (score %d)" + (length (cg-fish--captured game 0)) (aref (cg-get game :scores) 0))) + :stock-label "Deck" :stock (length (cg-get game :deck)) :discard 'none + :melds (list (cons "Table" (cg-rummy-sort-hand (cg-get game :table)))) + :hand (cg-fish--hand game 0) :cursor (cg-get game :cursor) + :message (cg-get game :message))) + (cl-defmethod cg-render ((game cg-fish-game)) - "Return a propertized depiction of the fishing GAME." + "Return a depiction of the fishing GAME: SVG board if graphical, else text." + (if (and cg-rummy-svg-cards (display-graphic-p)) + (cg-fish--svg game) + (cg-fish--render-text game))) + +(defun cg-fish--render-text (game) + "Return a plain-text depiction of the fishing GAME." (let* ((out '()) (cursor (cg-get game :cursor))) (push (format " %s to %d\n\n" (oref game vname) (oref game target)) out) (push (format " Computer: %d cards captured %d (score %d)\n" diff --git a/test/card-games-tests.el b/test/card-games-tests.el index 1492e3c..e0a9165 100644 --- a/test/card-games-tests.el +++ b/test/card-games-tests.el @@ -876,6 +876,24 @@ (should (rassoc '(hand . 0) regs)) (should (cl-every (lambda (r) (= 4 (length (car r)))) regs))))) +(ert-deftest cgt-hf-svg-smoke () + (let ((g (cg-handfoot-game))) + (cg-put g :nplayers 4) (cg-put g :nteams 2) (cg-put g :scores (make-vector 2 0)) + (cg-hf--deal g) + (cg-put g :cursor 0) (cg-put g :message "x") + (should (stringp (cg-hf--render-text g))) + (let ((regs (get-text-property 0 'cg-regions (cg-hf--svg g)))) + (should (rassoc '(hand . 0) regs)) + (should (cl-every (lambda (r) (= 4 (length (car r)))) regs))))) + +(ert-deftest cgt-fish-svg-smoke () + (let ((g (cg-scopa-game))) + (cg-fish--deal-round g) + (cg-put g :cursor 0) (cg-put g :message "x") + (should (stringp (cg-fish--render-text g))) + (should (rassoc '(hand . 0) + (get-text-property 0 'cg-regions (cg-fish--svg g)))))) + (ert-deftest cgt-pat-golf-deal () (let ((g (cg-pat--deal (cg-golf-game)))) (should (= 35 (length (cg-get g :cards))))