Add the rummy family: meld engine + Gin, Rummy, Rummy 500, Hand & Foot
Introduce a shared meld engine and four games built on it, all on cg-core/EIEIO with console UNICODE rendering. * cg-rummy.el: the meld engine and Gin Rummy. Set/run validation, candidate-meld enumeration, a bitmask-DP minimum-deadwood partition, and a layoff finder, plus the abstract cg-rummy-game base and shared render helpers. Gin is two-handed with draw/take/discard/knock, gin and undercut bonuses, opponent layoffs, and play to 100. * cg-rum500.el: the abstract cg-tablemeld-game (one mode and command set, dispatching on the subclass) driving Basic Rummy (meld out; score the cards left in other hands; to 100) and Rummy 500 (score the cards you lay down, lose those left in hand; ace high and worth 15; to 500). * cg-handfoot.el: Hand & Foot, a partnership Canasta cousin. Hand and foot packets, Twos and Jokers wild, rank books with clean/dirty piles, go-out bonus, and partnership scoring to 5000. Deliberately
This commit is contained in:
parent
b5410e1830
commit
86c44a362a
8 changed files with 1782 additions and 2 deletions
2
Makefile
2
Makefile
|
|
@ -3,7 +3,7 @@ EMACS ?= emacs
|
|||
PKG = card-games
|
||||
VERSION = 1.0.60
|
||||
# Source files in dependency order (cg-core first).
|
||||
EL = cg-core.el cg-svg.el cg-render.el cg-net.el cg-bid.el cg-gaps.el cg-bid-ui.el cg-bid-net.el cg-solitaire.el cg-trick.el cg-eights.el cg-patience.el cg-president.el card-games.el
|
||||
EL = cg-core.el cg-svg.el cg-render.el cg-net.el cg-bid.el cg-gaps.el cg-bid-ui.el cg-bid-net.el cg-solitaire.el cg-trick.el cg-eights.el cg-patience.el cg-president.el cg-rummy.el cg-rum500.el cg-handfoot.el card-games.el
|
||||
ELC = $(EL:.el=.elc)
|
||||
PKGDESC = $(PKG)-pkg.el
|
||||
TARDIR = $(PKG)-$(VERSION)
|
||||
|
|
|
|||
14
README.org
14
README.org
|
|
@ -60,6 +60,20 @@ with its command.
|
|||
beat it or pass; first out rules, last out scrubs, and the roles trade
|
||||
cards on the next deal.
|
||||
|
||||
** Rummy
|
||||
- ~cg-gin~ -- Gin Rummy. A two-handed duel: draw or take the discard,
|
||||
build sets and runs, and knock once your deadwood is ten or less, or go
|
||||
gin with none; your opponent then lays off and may undercut you. First
|
||||
to 100 wins.
|
||||
- ~cg-rummy-basic~ -- Rummy. Meld sets and runs onto the table and lay
|
||||
cards off onto them; empty your hand to go out and score the cards left
|
||||
in the other hands.
|
||||
- ~cg-rum500~ -- Rummy 500. As above, but you score the cards you lay
|
||||
down and lose the cards left in your hand; first past 500 wins.
|
||||
- ~cg-handfoot~ -- Hand & Foot. A partnership Canasta cousin: play a hand
|
||||
and then a foot, build books of a rank with Twos and Jokers wild, and go
|
||||
out once your side has completed two of them.
|
||||
|
||||
* TODO
|
||||
- [X] make the suit symbols customizable (~cg-symbols~) and obey them
|
||||
- [ ] a Texinfo manual
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@
|
|||
(require 'cg-eights)
|
||||
(require 'cg-patience)
|
||||
(require 'cg-president)
|
||||
(require 'cg-rummy)
|
||||
(require 'cg-rum500)
|
||||
(require 'cg-handfoot)
|
||||
|
||||
(defvar card-games-list
|
||||
'(("500 (Bid)" cg-bid
|
||||
|
|
@ -88,7 +91,15 @@
|
|||
("Oh Hell" cg-ohhell
|
||||
"Trick-taking: shrinking hands; bid the exact tricks you will take.")
|
||||
("President" cg-president
|
||||
"Climbing: shed your hand; first out rules, last out scrubs."))
|
||||
"Climbing: shed your hand; first out rules, last out scrubs.")
|
||||
("Gin Rummy" cg-gin
|
||||
"Rummy: form melds, knock with little deadwood; head to head to 100.")
|
||||
("Rummy" cg-rummy-basic
|
||||
"Rummy: meld your whole hand to the table to go out.")
|
||||
("Rummy 500" cg-rum500
|
||||
"Rummy: score the cards you lay down; race past 500.")
|
||||
("Hand & Foot" cg-handfoot
|
||||
"Rummy: partnership Canasta cousin; build books from hand and foot."))
|
||||
"Registry of playable games.
|
||||
Each entry is (NAME COMMAND DESCRIPTION); `card-game' lists them.")
|
||||
|
||||
|
|
|
|||
535
cg-handfoot.el
Normal file
535
cg-handfoot.el
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
;;; cg-handfoot.el --- Hand and Foot, a partnership rummy -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2026 Corwin Brust
|
||||
|
||||
;; Author: Corwin Brust <corwin@bru.st>
|
||||
;; Maintainer: Corwin Brust <corwin@bru.st>
|
||||
;; Version: 1.0.60
|
||||
;; Package-Requires: ((emacs "26.1"))
|
||||
;; Keywords: games
|
||||
;; URL: https://code.bru.st/corwin/card-game.el
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Hand & Foot, a Canasta cousin played in partnerships. Each player is
|
||||
;; dealt two packets: a "hand" played first and a "foot" taken up once the
|
||||
;; hand is gone. Partners build *books* -- three or more cards of one
|
||||
;; rank, suits ignored -- on the table; a book of seven is complete (a
|
||||
;; "pile"), clean if it holds no wild card and dirty if it does. Twos and
|
||||
;; Jokers are wild. You go out, ending the round, once your side owns at
|
||||
;; least two complete books and you can empty your foot.
|
||||
;;
|
||||
;; You partner the North player against East and West, all three of them
|
||||
;; computer opponents. Mark cards with SPC, meld them with m, lay off onto
|
||||
;; a book with l, and discard with RET.
|
||||
;;
|
||||
;; This is a deliberately streamlined Hand & Foot: it omits picking up the
|
||||
;; discard pile, the red-three bonus, and round-by-round minimum-meld
|
||||
;; requirements, keeping the books, wilds, hand/foot, and partnership
|
||||
;; scoring that give the game its character. Cards use the package cons
|
||||
;; (SUIT . RANK), RANK 0 (Ace) .. 12 (King), with jokers as (joker . 0).
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'eieio)
|
||||
(require 'cg-core)
|
||||
(require 'cg-rummy)
|
||||
|
||||
(defcustom cg-handfoot-target 5000
|
||||
"Points a partnership needs to win Hand & Foot."
|
||||
:type 'integer :group 'card-games)
|
||||
|
||||
(defconst cg-handfoot--names ["You" "West" "North" "East"]
|
||||
"Seat labels; North is your partner.")
|
||||
|
||||
(defclass cg-handfoot-game (cg-rummy-game)
|
||||
((vname :initform "Hand & Foot"))
|
||||
"A game of Hand & Foot.")
|
||||
|
||||
;;;; Cards
|
||||
|
||||
(defun cg-hf--wild-p (card)
|
||||
"Return non-nil when CARD is wild (a Joker or a Two)."
|
||||
(or (cg-rummy-joker-p card) (= (cdr card) 1)))
|
||||
|
||||
(defun cg-hf--three-p (card)
|
||||
"Return non-nil when CARD is a three (never meldable)."
|
||||
(and (not (cg-rummy-joker-p card)) (= (cdr card) 2)))
|
||||
|
||||
(defun cg-hf-value (card)
|
||||
"Return the Hand & Foot point value of CARD."
|
||||
(cond ((cg-rummy-joker-p card) 50)
|
||||
(t (let ((r (cdr card)))
|
||||
(cond ((= r 1) 20) ; Two (wild)
|
||||
((= r 0) 20) ; Ace
|
||||
((= r 2) 5) ; Three
|
||||
((<= r 6) 5) ; 4 5 6 7
|
||||
(t 10)))))) ; 8 9 10 J Q K
|
||||
|
||||
(defun cg-hf--book-rank (cards)
|
||||
"Return the natural rank shared by CARDS, or nil if invalid."
|
||||
(let ((nats (cl-remove-if #'cg-hf--wild-p cards)))
|
||||
(and nats
|
||||
(let ((r (cdr (car nats))))
|
||||
(and (cl-every (lambda (c) (= (cdr c) r)) nats)
|
||||
(/= r 2)
|
||||
r)))))
|
||||
|
||||
(defun cg-hf--book-valid-p (cards)
|
||||
"Return non-nil when CARDS form a legal book."
|
||||
(let* ((nats (cl-remove-if #'cg-hf--wild-p cards))
|
||||
(wilds (cl-remove-if-not #'cg-hf--wild-p cards)))
|
||||
(and (>= (length cards) 3)
|
||||
(cg-hf--book-rank cards)
|
||||
(>= (length nats) 2)
|
||||
(<= (length wilds) 3)
|
||||
(<= (length wilds) (length nats)))))
|
||||
|
||||
(defun cg-hf--book-complete-p (cards) (>= (length cards) 7))
|
||||
(defun cg-hf--book-clean-p (cards) (not (cl-some #'cg-hf--wild-p cards)))
|
||||
|
||||
;;;; Setup
|
||||
|
||||
(defun cg-hf--team (game s) (mod s (cg-get game :nteams)))
|
||||
|
||||
(cl-defmethod cg-hf--deal ((game cg-handfoot-game))
|
||||
"Deal a fresh round into GAME."
|
||||
(let* ((n (cg-get game :nplayers))
|
||||
(decks (1+ n))
|
||||
(deck (cg-rummy-deck decks 2))
|
||||
(hands (make-vector n nil)) (feet (make-vector n nil))
|
||||
(stage (make-vector n 0)))
|
||||
(dotimes (s n)
|
||||
(aset hands s (cg-rummy-sort-hand (cl-loop repeat 11 collect (pop deck))))
|
||||
(aset feet s (cg-rummy-sort-hand (cl-loop repeat 11 collect (pop deck)))))
|
||||
(cg-put game :hands hands)
|
||||
(cg-put game :feet feet)
|
||||
(cg-put game :stage stage)
|
||||
(cg-put game :books (make-vector (cg-get game :nteams) nil))
|
||||
(cg-put game :discard (list (pop deck)))
|
||||
(cg-put game :stock deck)
|
||||
(cg-put game :turn 0)
|
||||
(cg-put game :step 'draw)
|
||||
(cg-put game :phase 'play)
|
||||
(cg-put game :cursor 0)
|
||||
(cg-put game :marks nil)
|
||||
(cg-put game :message "Your turn: s draws two cards.")
|
||||
game))
|
||||
|
||||
(defun cg-hf--books (game team) (aref (cg-get game :books) team))
|
||||
(defun cg-hf--set-books (game team v) (aset (cg-get game :books) team v))
|
||||
|
||||
;;;; Engine
|
||||
|
||||
(defun cg-hf--draw2 (game s)
|
||||
"Draw two stock cards into seat S's hand. Return nil if stock runs out."
|
||||
(let ((ok t))
|
||||
(dotimes (_ 2)
|
||||
(let ((stock (cg-get game :stock)))
|
||||
(if (null stock) (setq ok nil)
|
||||
(cg-rummy--set-hand game s (cg-rummy-sort-hand
|
||||
(cons (car stock) (cg-rummy--hand game s))))
|
||||
(cg-put game :stock (cdr stock)))))
|
||||
ok))
|
||||
|
||||
(defun cg-hf--meld (game s cards)
|
||||
"Have seat S lay CARDS as a new book for their team. Return non-nil on win."
|
||||
(when (and (cg-hf--book-valid-p cards)
|
||||
(cl-subsetp cards (cg-rummy--hand game s) :test #'equal))
|
||||
(let ((team (cg-hf--team game s)))
|
||||
(dolist (c cards)
|
||||
(cg-rummy--set-hand game s (cg-rummy--remove1 c (cg-rummy--hand game s))))
|
||||
(cg-hf--set-books game team
|
||||
(append (cg-hf--books game team)
|
||||
(list (cg-rummy-sort-hand (copy-sequence cards)))))
|
||||
t)))
|
||||
|
||||
(defun cg-rummy--remove1 (card list)
|
||||
"Return LIST with one copy of CARD (an `equal' match) removed."
|
||||
(let ((seen nil))
|
||||
(cl-remove-if (lambda (c) (and (not seen) (equal c card) (setq seen t))) list)))
|
||||
|
||||
(defun cg-hf--layoff (game s card)
|
||||
"Lay CARD off onto a team book it fits. Return non-nil on success."
|
||||
(let* ((team (cg-hf--team game s)) (books (cg-hf--books game team)) (done nil))
|
||||
(catch 'hit
|
||||
(dolist (bk books)
|
||||
(unless (cg-hf--book-complete-p bk)
|
||||
(let ((cand (cons card bk)))
|
||||
(when (cg-hf--book-valid-p cand)
|
||||
(cg-rummy--set-hand game s (cg-rummy--remove1 card (cg-rummy--hand game s)))
|
||||
(setcar (memq bk books) (cg-rummy-sort-hand cand))
|
||||
(setq done t)
|
||||
(throw 'hit t))))))
|
||||
done))
|
||||
|
||||
(defun cg-hf--advance (game s)
|
||||
"After a play, take up the foot or finish, then pass the turn."
|
||||
(let ((stage (cg-get game :stage)))
|
||||
(when (and (= (aref stage s) 0) (null (cg-rummy--hand game s)))
|
||||
;; hand exhausted: pick up the foot
|
||||
(aset stage s 1)
|
||||
(cg-rummy--set-hand game s (aref (cg-get game :feet) s)))
|
||||
(if (and (= (aref stage s) 1) (null (cg-rummy--hand game s))
|
||||
(cg-hf--can-go-out-p game (cg-hf--team game s)))
|
||||
(cg-hf--score-round game s)
|
||||
(cg-put game :turn (mod (1+ s) (cg-get game :nplayers)))
|
||||
(cg-put game :step 'draw))))
|
||||
|
||||
(defun cg-hf--can-go-out-p (game team)
|
||||
"Return non-nil when TEAM owns at least two complete books."
|
||||
(>= (cl-count-if #'cg-hf--book-complete-p (cg-hf--books game team)) 2))
|
||||
|
||||
(defun cg-hf--discard (game s card)
|
||||
"Discard CARD from seat S and end the play portion of the turn."
|
||||
(cg-rummy--set-hand game s (cg-rummy--remove1 card (cg-rummy--hand game s)))
|
||||
(cg-put game :discard (cons card (cg-get game :discard)))
|
||||
(cg-hf--advance game s))
|
||||
|
||||
(cl-defmethod cg-hf--score-round ((game cg-handfoot-game) outseat)
|
||||
"Score the round (OUTSEAT went out, or nil if the stock ran dry)."
|
||||
(let* ((nt (cg-get game :nteams)) (scores (cg-get game :scores)))
|
||||
(dotimes (team nt)
|
||||
(let ((pts 0))
|
||||
(dolist (bk (cg-hf--books game team))
|
||||
(dolist (c bk) (setq pts (+ pts (cg-hf-value c))))
|
||||
(when (cg-hf--book-complete-p bk)
|
||||
(setq pts (+ pts (if (cg-hf--book-clean-p bk) 500 300)))))
|
||||
(when (and outseat (= (cg-hf--team game outseat) team))
|
||||
(setq pts (+ pts 100))) ; going-out bonus
|
||||
;; subtract cards left in members' hands and feet
|
||||
(dotimes (s (cg-get game :nplayers))
|
||||
(when (= (cg-hf--team game s) team)
|
||||
(dolist (c (cg-rummy--hand game s)) (setq pts (- pts (cg-hf-value c))))
|
||||
(when (= (aref (cg-get game :stage) s) 0)
|
||||
(dolist (c (aref (cg-get game :feet) s))
|
||||
(setq pts (- pts (cg-hf-value c)))))))
|
||||
(aset scores team (+ (aref scores team) pts))))
|
||||
(let ((win nil) (best most-negative-fixnum))
|
||||
(dotimes (team nt)
|
||||
(when (and (>= (aref scores team) cg-handfoot-target)
|
||||
(> (aref scores team) best))
|
||||
(setq win team best (aref scores team))))
|
||||
(cg-put game :phase (if win 'game-over 'round-over))
|
||||
(cg-put game :winner (or win (and outseat (cg-hf--team game outseat))))
|
||||
(cg-put game :reveal t)
|
||||
(cg-put game :message
|
||||
(concat
|
||||
(if outseat (format "%s goes out! " (aref cg-handfoot--names outseat))
|
||||
"Stock exhausted. ")
|
||||
(if win (format "Team %d wins the game! (n: new game)" win)
|
||||
(format "Round over. Scores: %s. (n: next round)"
|
||||
(cg-hf--scores-string game))))))))
|
||||
|
||||
(defun cg-hf--scores-string (game)
|
||||
"Return a compact per-team score line for GAME."
|
||||
(let ((scores (cg-get game :scores)) (parts '()))
|
||||
(dotimes (team (cg-get game :nteams))
|
||||
(push (format "Team %d %d" team (aref scores team)) parts))
|
||||
(mapconcat #'identity (nreverse parts) " · ")))
|
||||
|
||||
;;;; AI
|
||||
|
||||
(defun cg-hf--ai-meld (game s)
|
||||
"Lay down and extend books for seat S as far as is easy."
|
||||
;; lay off naturals onto existing incomplete team books
|
||||
(let ((again t))
|
||||
(while again
|
||||
(setq again nil)
|
||||
(let ((card (cl-find-if
|
||||
(lambda (c)
|
||||
(and (not (cg-hf--wild-p c)) (not (cg-hf--three-p c))
|
||||
(cl-find-if
|
||||
(lambda (bk) (and (not (cg-hf--book-complete-p bk))
|
||||
(equal (cg-hf--book-rank bk) (cdr c))))
|
||||
(cg-hf--books game (cg-hf--team game s)))))
|
||||
(cg-rummy--hand game s))))
|
||||
(when card (cg-hf--layoff game s card) (setq again t)))))
|
||||
;; start new books from ranks with >=3 naturals in hand
|
||||
(let ((again t))
|
||||
(while again
|
||||
(setq again nil)
|
||||
(let* ((hand (cg-rummy--hand game s))
|
||||
(byrank (make-hash-table :test 'eql)) (target nil))
|
||||
(dolist (c hand)
|
||||
(unless (or (cg-hf--wild-p c) (cg-hf--three-p c))
|
||||
(push c (gethash (cdr c) byrank))))
|
||||
(maphash (lambda (_r cs) (when (and (not target) (>= (length cs) 3))
|
||||
(setq target cs)))
|
||||
byrank)
|
||||
(when target
|
||||
(cg-hf--meld game s (cl-subseq target 0 (min 7 (length target))))
|
||||
(setq again t)))))
|
||||
;; push a nearly-complete book to seven with a spare wild
|
||||
(let ((again t))
|
||||
(while again
|
||||
(setq again nil)
|
||||
(let ((wild (cl-find-if #'cg-hf--wild-p (cg-rummy--hand game s)))
|
||||
(team (cg-hf--team game s)))
|
||||
(when wild
|
||||
(let ((bk (cl-find-if
|
||||
(lambda (b)
|
||||
(and (not (cg-hf--book-complete-p b))
|
||||
(>= (length b) 5)
|
||||
(> (cl-count-if-not #'cg-hf--wild-p b)
|
||||
(cl-count-if #'cg-hf--wild-p b))
|
||||
(< (cl-count-if #'cg-hf--wild-p b) 3)))
|
||||
(cg-hf--books game team))))
|
||||
(when bk
|
||||
(cg-rummy--set-hand game s (cg-rummy--remove1 wild (cg-rummy--hand game s)))
|
||||
(setcar (memq bk (cg-hf--books game team))
|
||||
(cg-rummy-sort-hand (cons wild bk)))
|
||||
(setq again t))))))))
|
||||
|
||||
(defun cg-hf--ai-discard-card (game s)
|
||||
"Return the card seat S should discard."
|
||||
(let ((hand (cg-rummy--hand game s)))
|
||||
(or (cl-find-if #'cg-hf--three-p hand)
|
||||
;; a high singleton, else the first card
|
||||
(let ((byrank (make-hash-table :test 'eql)) (best (car hand)) (bestv -1))
|
||||
(dolist (c hand) (unless (cg-hf--wild-p c)
|
||||
(push c (gethash (cdr c) byrank))))
|
||||
(maphash (lambda (_r cs)
|
||||
(when (= (length cs) 1)
|
||||
(let ((v (cg-hf-value (car cs))))
|
||||
(when (> v bestv) (setq best (car cs) bestv v)))))
|
||||
byrank)
|
||||
best))))
|
||||
|
||||
(cl-defmethod cg-hf--ai-turn ((game cg-handfoot-game) s)
|
||||
"Play seat S's whole turn."
|
||||
(if (not (cg-hf--draw2 game s))
|
||||
(cg-hf--score-round game nil)
|
||||
(cg-hf--ai-meld game s)
|
||||
(when (eq (cg-get game :phase) 'play)
|
||||
;; if the hand emptied through melding, pick up the foot and meld again
|
||||
(when (and (= (aref (cg-get game :stage) s) 0) (null (cg-rummy--hand game s)))
|
||||
(aset (cg-get game :stage) s 1)
|
||||
(cg-rummy--set-hand game s (aref (cg-get game :feet) s))
|
||||
(cg-hf--ai-meld game s))
|
||||
(when (eq (cg-get game :phase) 'play)
|
||||
(if (cg-rummy--hand game s)
|
||||
(cg-hf--discard game s (cg-hf--ai-discard-card game s))
|
||||
(cg-hf--advance game s))))))
|
||||
|
||||
(defun cg-hf--run (game)
|
||||
"Advance AI seats until it is your turn or the round ends."
|
||||
(while (and (eq (cg-get game :phase) 'play) (/= (cg-get game :turn) 0))
|
||||
(cg-hf--ai-turn game (cg-get game :turn))))
|
||||
|
||||
;;;; UI
|
||||
|
||||
(defvar-local cg-hf--game nil "The Hand & Foot game in the current buffer.")
|
||||
|
||||
(cl-defmethod cg-render ((game cg-handfoot-game))
|
||||
"Return a propertized 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\n\n" cg-handfoot-target) out)
|
||||
(dotimes (team (cg-get game :nteams))
|
||||
(push (format " Team %d (score %d):\n" team (aref scores team)) out)
|
||||
(let ((bks (cg-hf--books game team)))
|
||||
(if bks
|
||||
(dolist (bk bks)
|
||||
(push (format " %s%s\n"
|
||||
(mapconcat #'cg-rummy-card-string bk " ")
|
||||
(cond ((cg-hf--book-complete-p bk)
|
||||
(if (cg-hf--book-clean-p bk) " [clean pile]" " [dirty pile]"))
|
||||
(t "")))
|
||||
out))
|
||||
(push " (no books yet)\n" out))))
|
||||
(push "\n" out)
|
||||
(dotimes (s (cg-get game :nplayers))
|
||||
(unless (= s 0)
|
||||
(push (format " %-6s %d in hand%s\n" (aref cg-handfoot--names s)
|
||||
(length (cg-rummy--hand game s))
|
||||
(if (= (aref (cg-get game :stage) s) 1) " (on foot)" ""))
|
||||
out)))
|
||||
(push (format "\n Discard: %s Stock: %d\n\n"
|
||||
(let ((cs (cg-rummy-card-string (cg-rummy--top game))) (tp (cg-rummy--top game)))
|
||||
(if (and tp (not (cg-rummy-joker-p tp)) (cg-red-suit-p (car tp)))
|
||||
(propertize cs 'face 'cg-red-suit) cs))
|
||||
(length (cg-get game :stock)))
|
||||
out)
|
||||
(push (format " Your %s:\n " (if (= (aref (cg-get game :stage) 0) 1) "foot" "hand")) out)
|
||||
(push (cg-rummy--render-cards hand cursor (cg-get game :marks)) out)
|
||||
(push (format "\n\n %s\n" (cg-get game :message)) out)
|
||||
(apply #'concat (nreverse out))))
|
||||
|
||||
(defun cg-hf--redisplay ()
|
||||
"Redraw the Hand & Foot buffer."
|
||||
(let ((game cg-hf--game) (inhibit-read-only t))
|
||||
(setq-local mode-line-process
|
||||
(format " [%s]" (or (cg-get game :step) (cg-get game :phase))))
|
||||
(erase-buffer) (insert (cg-render game)) (goto-char (point-min))))
|
||||
|
||||
(defun cg-hf--clamp (g)
|
||||
"Keep G's cursor in range and drop stale marks."
|
||||
(let ((n (length (cg-rummy--hand g 0))))
|
||||
(cg-put g :cursor (if (> n 0) (min (cg-get g :cursor) (1- n)) 0))
|
||||
(cg-put g :marks (cl-remove-if (lambda (i) (>= i n)) (cg-get g :marks)))))
|
||||
|
||||
(defun cg-hf--my-turn-p (g)
|
||||
(and (eq (cg-get g :phase) 'play) (= (cg-get g :turn) 0)))
|
||||
|
||||
(defun cg-hf-left ()
|
||||
"Move the hand cursor left."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1- (cg-get g :cursor)) n)))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-right ()
|
||||
"Move the hand cursor right."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1+ (cg-get g :cursor)) n)))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-mark ()
|
||||
"Toggle a mark on the card under the cursor."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (i (cg-get g :cursor)) (marks (cg-get g :marks)))
|
||||
(cg-put g :marks (if (memq i marks) (delq i marks) (cons i marks)))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf--marked (g)
|
||||
"Return the marked cards in G's hand."
|
||||
(let ((hand (cg-rummy--hand g 0)))
|
||||
(mapcar (lambda (i) (nth i hand)) (sort (copy-sequence (cg-get g :marks)) #'<))))
|
||||
|
||||
(defun cg-hf-meld ()
|
||||
"Meld the marked cards as a new book."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (cards (cg-hf--marked g)))
|
||||
(cond
|
||||
((not (cg-hf--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s)."))
|
||||
((not (cg-hf--book-valid-p cards))
|
||||
(cg-put g :message "Not a legal book: 3+ of a rank, 2+ natural, wilds <= naturals."))
|
||||
((cg-hf--meld g 0 cards)
|
||||
(cg-put g :marks nil) (cg-hf--clamp g)
|
||||
(when (and (= (aref (cg-get g :stage) 0) 0) (null (cg-rummy--hand g 0)))
|
||||
(aset (cg-get g :stage) 0 1)
|
||||
(cg-rummy--set-hand g 0 (aref (cg-get g :feet) 0))
|
||||
(cg-hf--clamp g)
|
||||
(cg-put g :message "Hand cleared -- foot picked up! Keep melding or discard (RET)."))
|
||||
(unless (and (= (aref (cg-get g :stage) 0) 1) (null (cg-rummy--hand g 0)))
|
||||
(cg-put g :message "Booked. Meld more, lay off (l), or discard (RET).")))
|
||||
(t (cg-put g :message "Could not meld those cards.")))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-layoff ()
|
||||
"Lay the cursor card (or marked cards) off onto a team book."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (marks (cg-hf--marked g)))
|
||||
(cond
|
||||
((not (cg-hf--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s)."))
|
||||
(t (let ((cards (or marks (list (nth (cg-get g :cursor) (cg-rummy--hand g 0)))))
|
||||
(any nil))
|
||||
(dolist (c cards) (when (and c (cg-hf--layoff g 0 c)) (setq any t)))
|
||||
(cg-put g :marks nil) (cg-hf--clamp g)
|
||||
(cg-put g :message (if any "Laid off." "That card fits none of your books.")))))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-draw ()
|
||||
"Draw two cards from the stock."
|
||||
(interactive)
|
||||
(let ((g cg-hf--game))
|
||||
(cond
|
||||
((not (cg-hf--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'draw)) (cg-put g :message "You already drew."))
|
||||
((cg-hf--draw2 g 0)
|
||||
(cg-put g :step 'play) (cg-hf--clamp g)
|
||||
(cg-put g :message "Meld (m), lay off (l), then discard (RET)."))
|
||||
(t (cg-hf--score-round g nil)))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-discard ()
|
||||
"Discard the cursor card and end your turn."
|
||||
(interactive)
|
||||
(let* ((g cg-hf--game) (card (nth (cg-get g :cursor) (cg-rummy--hand g 0))))
|
||||
(cond
|
||||
((not (cg-hf--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s)."))
|
||||
((null card) (cg-put g :message "No card selected."))
|
||||
(t (cg-hf--discard g 0 card)
|
||||
(cg-put g :marks nil)
|
||||
(when (memq (cg-get g :phase) '(play))
|
||||
(cg-put g :message "You discarded.")
|
||||
(cg-hf--run g))))
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-new ()
|
||||
"Deal a fresh round, or a new game when one is over."
|
||||
(interactive)
|
||||
(let ((g cg-hf--game))
|
||||
(when (eq (cg-get g :phase) 'game-over)
|
||||
(cg-put g :scores (make-vector (cg-get g :nteams) 0)))
|
||||
(cg-put g :reveal nil)
|
||||
(cg-hf--deal g)
|
||||
(cg-hf--run g)
|
||||
(cg-hf--redisplay)))
|
||||
|
||||
(defun cg-hf-redraw () "Redraw the board." (interactive) (cg-hf--redisplay))
|
||||
|
||||
(defun cg-hf-help ()
|
||||
"Describe the Hand & Foot controls."
|
||||
(interactive)
|
||||
(message "Arrows: choose SPC: mark m: meld l: lay off s: draw 2 RET: discard n: new"))
|
||||
|
||||
(defvar cg-handfoot-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "<left>") #'cg-hf-left)
|
||||
(define-key map (kbd "<right>") #'cg-hf-right)
|
||||
(define-key map (kbd "SPC") #'cg-hf-mark)
|
||||
(define-key map "m" #'cg-hf-meld)
|
||||
(define-key map "l" #'cg-hf-layoff)
|
||||
(define-key map "s" #'cg-hf-draw)
|
||||
(define-key map (kbd "RET") #'cg-hf-discard)
|
||||
(define-key map "n" #'cg-hf-new)
|
||||
(define-key map "g" #'cg-hf-redraw)
|
||||
(define-key map "?" #'cg-hf-help)
|
||||
map)
|
||||
"Keymap for `cg-handfoot-mode'.")
|
||||
|
||||
(define-derived-mode cg-handfoot-mode special-mode "Hand&Foot"
|
||||
"Major mode for Hand & Foot."
|
||||
(setq-local truncate-lines t))
|
||||
|
||||
;;;###autoload
|
||||
(defun cg-handfoot ()
|
||||
"Play Hand & Foot, partnering North against two AI opponents."
|
||||
(interactive)
|
||||
(let ((buf (get-buffer-create "*Hand & Foot*")))
|
||||
(with-current-buffer buf
|
||||
(cg-handfoot-mode)
|
||||
(setq cg-hf--game (cg-handfoot-game))
|
||||
(cg-put cg-hf--game :nplayers 4)
|
||||
(cg-put cg-hf--game :nteams 2)
|
||||
(cg-put cg-hf--game :scores (make-vector 2 0))
|
||||
(cg-hf--deal cg-hf--game)
|
||||
(cg-hf--run cg-hf--game)
|
||||
(cg-hf--redisplay))
|
||||
(switch-to-buffer buf)))
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'cg-hand-and-foot #'cg-handfoot)
|
||||
|
||||
(provide 'cg-handfoot)
|
||||
;;; cg-handfoot.el ends here
|
||||
519
cg-rum500.el
Normal file
519
cg-rum500.el
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
;;; cg-rum500.el --- Basic Rummy and Rummy 500 -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2026 Corwin Brust
|
||||
|
||||
;; Author: Corwin Brust <corwin@bru.st>
|
||||
;; Maintainer: Corwin Brust <corwin@bru.st>
|
||||
;; Version: 1.0.60
|
||||
;; Package-Requires: ((emacs "26.1"))
|
||||
;; Keywords: games
|
||||
;; URL: https://code.bru.st/corwin/card-game.el
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Two table-meld rummy games sharing one engine, built on the meld
|
||||
;; finder in cg-rummy.el.
|
||||
;;
|
||||
;; In a table-meld game you draw a card, lay melds face-up on the table,
|
||||
;; lay single cards off onto melds already there, and end your turn by
|
||||
;; discarding. Empty your hand to go out.
|
||||
;;
|
||||
;; `cg-rummy-basic' -- plain Rummy: the first player to meld their whole
|
||||
;; hand wins the deal and scores the cards left in the others' hands.
|
||||
;; `cg-rum500' -- Rummy 500: you score the cards you lay down and lose
|
||||
;; the cards left in your hand; first past 500 wins.
|
||||
;;
|
||||
;; You are the South player (seat 0); the rest are simple AI. To meld,
|
||||
;; mark cards with SPC and press m; to lay a card off, put the cursor on
|
||||
;; it and press l.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'eieio)
|
||||
(require 'cg-core)
|
||||
(require 'cg-rummy)
|
||||
|
||||
(defclass cg-tablemeld-game (cg-rummy-game)
|
||||
((nplayers :initarg :nplayers :initform 2)
|
||||
(hand-size :initarg :hand-size :initform 10)
|
||||
(ace-15 :initarg :ace-15 :initform nil)
|
||||
(ace-high :initarg :ace-high :initform nil)
|
||||
(target :initarg :target :initform 100)
|
||||
(score-style :initarg :score-style :initform 'go-out))
|
||||
"Abstract base for table-meld rummy games (Basic Rummy, Rummy 500)."
|
||||
:abstract t)
|
||||
|
||||
;;;; Engine
|
||||
|
||||
(cl-defmethod cg-tm--deal ((game cg-tablemeld-game))
|
||||
"Deal a fresh hand into GAME."
|
||||
(let* ((n (oref game nplayers))
|
||||
(deck (cg-rummy-deck))
|
||||
(per (oref game hand-size))
|
||||
(hands (make-vector n nil)))
|
||||
(dotimes (s n)
|
||||
(aset hands s (cg-rummy-sort-hand (cl-loop repeat per collect (pop deck)))))
|
||||
(cg-put game :hands hands)
|
||||
(cg-put game :nplayers n)
|
||||
(cg-put game :discard (list (pop deck)))
|
||||
(cg-put game :stock deck)
|
||||
(cg-put game :table nil)
|
||||
(cg-put game :laid (make-vector n 0))
|
||||
(cg-put game :turn 0)
|
||||
(cg-put game :step 'draw)
|
||||
(cg-put game :phase 'play)
|
||||
(cg-put game :cursor 0)
|
||||
(cg-put game :marks nil)
|
||||
(unless (cg-get game :scores) (cg-put game :scores (make-vector n 0)))
|
||||
(cg-put game :message "Your turn: s draws from stock, t takes the discard.")
|
||||
game))
|
||||
|
||||
(defun cg-tm--ace-high (game) (oref game ace-high))
|
||||
(defun cg-tm--ace-15 (game) (oref game ace-15))
|
||||
|
||||
(defun cg-tm--draw (game s)
|
||||
"Move one stock card to seat S's hand; return it or nil if stock empty."
|
||||
(let ((stock (cg-get game :stock)))
|
||||
(when stock
|
||||
(let ((c (pop stock)))
|
||||
(cg-put game :stock stock)
|
||||
(cg-rummy--set-hand game s (cg-rummy-sort-hand
|
||||
(cons c (cg-rummy--hand game s))))
|
||||
c))))
|
||||
|
||||
(defun cg-tm--take-top (game s)
|
||||
"Move the discard top to seat S's hand and return it."
|
||||
(let ((c (cg-rummy--top game)))
|
||||
(when c
|
||||
(cg-put game :discard (cdr (cg-get game :discard)))
|
||||
(cg-rummy--set-hand game s (cg-rummy-sort-hand
|
||||
(cons c (cg-rummy--hand game s))))
|
||||
c)))
|
||||
|
||||
(defun cg-tm--meld-value (game cards)
|
||||
"Return the total point value of CARDS for GAME's scoring."
|
||||
(apply #'+ (mapcar (lambda (c) (cg-rummy-value c (cg-tm--ace-15 game))) cards)))
|
||||
|
||||
(cl-defmethod cg-tm--meld ((game cg-tablemeld-game) s cards)
|
||||
"Have seat S meld CARDS onto the table. Return non-nil on success."
|
||||
(when (cg-rummy-meld-p cards :min 3 :ace-high (cg-tm--ace-high game)
|
||||
:distinct-suits t)
|
||||
(dolist (c cards) (cg-rummy--set-hand game s (remove c (cg-rummy--hand game s))))
|
||||
(cg-put game :table (append (cg-get game :table)
|
||||
(list (cons s (copy-sequence cards)))))
|
||||
(let ((laid (cg-get game :laid)))
|
||||
(aset laid s (+ (aref laid s) (cg-tm--meld-value game cards))))
|
||||
t))
|
||||
|
||||
(cl-defmethod cg-tm--layoff ((game cg-tablemeld-game) s card)
|
||||
"Have seat S lay CARD off onto a matching table meld. Return non-nil on success."
|
||||
(let ((rec (cl-find-if
|
||||
(lambda (r) (cg-rummy-meld-p (cons card (cdr r)) :min 3
|
||||
:ace-high (cg-tm--ace-high game)))
|
||||
(cg-get game :table))))
|
||||
(when rec
|
||||
(cg-rummy--set-hand game s (remove card (cg-rummy--hand game s)))
|
||||
(setcdr rec (cg-rummy-sort-hand (cons card (cdr rec))))
|
||||
(let ((laid (cg-get game :laid)))
|
||||
(aset laid s (+ (aref laid s) (cg-rummy-value card (cg-tm--ace-15 game)))))
|
||||
t)))
|
||||
|
||||
(cl-defmethod cg-tm--score-hand ((game cg-tablemeld-game) outseat)
|
||||
"Score the hand ended by OUTSEAT (or nil for a washed-out hand)."
|
||||
(let* ((n (cg-get game :nplayers)) (scores (cg-get game :scores))
|
||||
(style (oref game score-style)))
|
||||
(cond
|
||||
((eq style 'go-out)
|
||||
(when outseat
|
||||
(let ((sum 0))
|
||||
(dotimes (s n)
|
||||
(unless (= s outseat)
|
||||
(dolist (c (cg-rummy--hand game s))
|
||||
(setq sum (+ sum (cg-rummy-value c))))))
|
||||
(aset scores outseat (+ (aref scores outseat) sum)))))
|
||||
((eq style 'meld-points)
|
||||
(let ((laid (cg-get game :laid)))
|
||||
(dotimes (s n)
|
||||
(let ((rem (apply #'+ (mapcar (lambda (c)
|
||||
(cg-rummy-value c (cg-tm--ace-15 game)))
|
||||
(cg-rummy--hand game s)))))
|
||||
(aset scores s (+ (aref scores s) (- (aref laid s) rem))))))))
|
||||
;; decide if the game is over
|
||||
(let ((win nil) (best most-negative-fixnum))
|
||||
(dotimes (s n)
|
||||
(when (and (>= (aref scores s) (oref game target))
|
||||
(> (aref scores s) best))
|
||||
(setq win s best (aref scores s))))
|
||||
(cg-put game :phase (if win 'game-over 'hand-over))
|
||||
(cg-put game :winner (or win outseat))
|
||||
(cg-put game :reveal t)
|
||||
(cg-put game :message
|
||||
(if win
|
||||
(format "%s wins the game with %d! (n: new game)"
|
||||
(cg-tm--who win) (aref scores win))
|
||||
(concat (if outseat (format "%s goes out. " (cg-tm--who outseat))
|
||||
"Stock exhausted. ")
|
||||
(format "Scores: %s. (n: next hand)"
|
||||
(cg-tm--scores-string game))))))))
|
||||
|
||||
(defun cg-tm--who (s) (if (= s 0) "You" (format "Player %d" s)))
|
||||
|
||||
(defun cg-tm--scores-string (game)
|
||||
"Return a compact \"You N · P1 N ...\" score line for GAME."
|
||||
(let ((scores (cg-get game :scores)) (parts '()))
|
||||
(dotimes (s (cg-get game :nplayers))
|
||||
(push (format "%s %d" (if (= s 0) "You" (format "P%d" s)) (aref scores s))
|
||||
parts))
|
||||
(mapconcat #'identity (nreverse parts) " · ")))
|
||||
|
||||
(cl-defmethod cg-tm--end-turn ((game cg-tablemeld-game) s)
|
||||
"Finish seat S's turn: go out if the hand is empty, else advance."
|
||||
(if (null (cg-rummy--hand game s))
|
||||
(cg-tm--score-hand game s)
|
||||
(cg-put game :turn (mod (1+ s) (cg-get game :nplayers)))
|
||||
(cg-put game :step 'draw)))
|
||||
|
||||
(cl-defmethod cg-tm--discard ((game cg-tablemeld-game) s card)
|
||||
"Discard CARD from seat S and finish the turn."
|
||||
(cg-rummy--set-hand game s (remove card (cg-rummy--hand game s)))
|
||||
(cg-put game :discard (cons card (cg-get game :discard)))
|
||||
(cg-tm--end-turn game s))
|
||||
|
||||
;;;; AI
|
||||
|
||||
(defun cg-tm--ai-melds (game s)
|
||||
"Lay down every meld seat S can, keeping a card back to discard.
|
||||
Return non-nil if any meld was laid."
|
||||
(let ((did nil) (again t))
|
||||
(while again
|
||||
(setq again nil)
|
||||
(let* ((hand (cg-rummy--hand game s))
|
||||
(p (cg-rummy-best-partition hand :ace-high (cg-tm--ace-high game)
|
||||
:ace-15 (cg-tm--ace-15 game)))
|
||||
(melds (plist-get p :melds))
|
||||
;; keep one card to discard: skip a meld if it would empty the hand
|
||||
(melded (apply #'+ (mapcar #'length melds))))
|
||||
(when (and melds (= melded (length hand)))
|
||||
(setq melds (cdr (sort melds (lambda (a b) (< (length a) (length b)))))))
|
||||
(when melds
|
||||
(cg-tm--meld game s (car melds))
|
||||
(setq did t again t))))
|
||||
did))
|
||||
|
||||
(defun cg-tm--ai-layoffs (game s)
|
||||
"Lay off every fitting card from seat S, keeping a card back to discard."
|
||||
(let ((again t))
|
||||
(while again
|
||||
(setq again nil)
|
||||
(when (> (length (cg-rummy--hand game s)) 1)
|
||||
(let ((card (cl-find-if
|
||||
(lambda (c)
|
||||
(cl-find-if
|
||||
(lambda (r) (cg-rummy-meld-p (cons c (cdr r)) :min 3
|
||||
:ace-high (cg-tm--ace-high game)))
|
||||
(cg-get game :table)))
|
||||
(cg-rummy--hand game s))))
|
||||
(when card (cg-tm--layoff game s card) (setq again t)))))))
|
||||
|
||||
(defun cg-tm--ai-discard-card (game s)
|
||||
"Return the best card for seat S to discard (highest deadwood)."
|
||||
(let* ((hand (cg-rummy--hand game s))
|
||||
(p (cg-rummy-best-partition hand :ace-high (cg-tm--ace-high game)
|
||||
:ace-15 (cg-tm--ace-15 game)))
|
||||
(dead (or (plist-get p :deadwood) hand))
|
||||
(best (car dead)) (bestv -1))
|
||||
(dolist (c dead best)
|
||||
(let ((v (cg-rummy-value c (cg-tm--ace-15 game))))
|
||||
(when (> v bestv) (setq best c bestv v))))))
|
||||
|
||||
(cl-defmethod cg-tm--ai-turn ((game cg-tablemeld-game) s)
|
||||
"Play seat S's whole turn."
|
||||
(let* ((hand (cg-rummy--hand game s))
|
||||
(up (cg-rummy--top game))
|
||||
(cur (cg-rummy-deadwood hand (cg-tm--ace-high game) (cg-tm--ace-15 game)))
|
||||
(with (and up (cg-rummy-deadwood (cons up hand)
|
||||
(cg-tm--ace-high game) (cg-tm--ace-15 game))))
|
||||
(drew (if (and up with (< with cur))
|
||||
(cg-tm--take-top game s)
|
||||
(cg-tm--draw game s))))
|
||||
(if (not drew)
|
||||
(cg-tm--score-hand game nil)
|
||||
(cg-tm--ai-melds game s)
|
||||
(cg-tm--ai-layoffs game s)
|
||||
(when (eq (cg-get game :phase) 'play)
|
||||
(if (null (cg-rummy--hand game s))
|
||||
(cg-tm--end-turn game s) ; melded out, no discard needed
|
||||
(cg-tm--discard game s (cg-tm--ai-discard-card game s)))))))
|
||||
|
||||
(defun cg-tm--run (game)
|
||||
"Advance AI seats until it is the human's turn or the hand ends."
|
||||
(while (and (eq (cg-get game :phase) 'play) (/= (cg-get game :turn) 0))
|
||||
(cg-tm--ai-turn game (cg-get game :turn))))
|
||||
|
||||
;;;; UI
|
||||
|
||||
(defvar-local cg-tm--game nil "The table-meld game in the current buffer.")
|
||||
|
||||
(defun cg-tm--layoff-hint (game)
|
||||
"Return a predicate marking cards that can be laid off in GAME now."
|
||||
(lambda (c)
|
||||
(cl-find-if (lambda (r) (cg-rummy-meld-p (cons c (cdr r)) :min 3
|
||||
:ace-high (cg-tm--ace-high game)))
|
||||
(cg-get game :table))))
|
||||
|
||||
(cl-defmethod cg-render ((game cg-tablemeld-game))
|
||||
"Return a propertized depiction of the table-meld GAME."
|
||||
(let* ((out '()) (scores (cg-get game :scores))
|
||||
(laid (cg-get game :laid)) (meldp (oref game score-style))
|
||||
(hand (cg-rummy--hand game 0)) (cursor (cg-get game :cursor)))
|
||||
(push (format " %s target %d\n\n" (oref game vname) (oref game target)) out)
|
||||
(dotimes (s (cg-get game :nplayers))
|
||||
(unless (= s 0)
|
||||
(push (format " Player %d: %d cards score %d%s\n"
|
||||
s (length (cg-rummy--hand game s)) (aref scores s)
|
||||
(if (eq meldp 'meld-points)
|
||||
(format " (laid %d)" (aref laid s)) ""))
|
||||
out)))
|
||||
(push "\n Table:\n" out)
|
||||
(if (cg-get game :table)
|
||||
(dolist (rec (cg-get game :table))
|
||||
(push (format " [%s] %s\n" (if (= (car rec) 0) "you" (format "P%d" (car rec)))
|
||||
(mapconcat #'cg-rummy-card-string (cdr rec) " "))
|
||||
out))
|
||||
(push " (empty)\n" out))
|
||||
(push (format "\n Discard: %s Stock: %d\n\n"
|
||||
(let ((cs (cg-rummy-card-string (cg-rummy--top game))) (tp (cg-rummy--top game)))
|
||||
(if (and tp (cg-red-suit-p (car tp))) (propertize cs 'face 'cg-red-suit) cs))
|
||||
(length (cg-get game :stock)))
|
||||
out)
|
||||
(push (format " Your hand%s:\n "
|
||||
(if (eq meldp 'meld-points) (format " (laid %d, score %d)"
|
||||
(aref laid 0) (aref scores 0))
|
||||
(format " (score %d)" (aref scores 0))))
|
||||
out)
|
||||
(push (cg-rummy--render-cards hand cursor (cg-get game :marks)
|
||||
(cg-tm--layoff-hint game))
|
||||
out)
|
||||
(push (format "\n\n %s\n" (cg-get game :message)) out)
|
||||
(apply #'concat (nreverse out))))
|
||||
|
||||
(defun cg-tm--redisplay ()
|
||||
"Redraw the table-meld buffer."
|
||||
(let ((game cg-tm--game) (inhibit-read-only t))
|
||||
(setq-local mode-line-process
|
||||
(format " [%s]" (or (cg-get game :step) (cg-get game :phase))))
|
||||
(erase-buffer) (insert (cg-render game)) (goto-char (point-min))))
|
||||
|
||||
(defun cg-tm--clamp-cursor (g)
|
||||
"Keep G's cursor within the hand and drop stale marks."
|
||||
(let ((n (length (cg-rummy--hand g 0))))
|
||||
(cg-put g :cursor (if (> n 0) (min (cg-get g :cursor) (1- n)) 0))
|
||||
(cg-put g :marks (cl-remove-if (lambda (i) (>= i n)) (cg-get g :marks)))))
|
||||
|
||||
(defun cg-tm--my-turn-p (g)
|
||||
(and (eq (cg-get g :phase) 'play) (= (cg-get g :turn) 0)))
|
||||
|
||||
(defun cg-tm-left ()
|
||||
"Move the hand cursor left."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1- (cg-get g :cursor)) n)))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-right ()
|
||||
"Move the hand cursor right."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1+ (cg-get g :cursor)) n)))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-mark ()
|
||||
"Toggle a mark on the card under the cursor (for melding)."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (i (cg-get g :cursor)) (marks (cg-get g :marks)))
|
||||
(cg-put g :marks (if (memq i marks) (delq i marks) (cons i marks)))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm--marked-cards (g)
|
||||
"Return the cards currently marked in G's hand."
|
||||
(let ((hand (cg-rummy--hand g 0)))
|
||||
(mapcar (lambda (i) (nth i hand)) (sort (copy-sequence (cg-get g :marks)) #'<))))
|
||||
|
||||
(defun cg-tm-meld ()
|
||||
"Meld the marked cards onto the table."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (cards (cg-tm--marked-cards g)))
|
||||
(cond
|
||||
((not (cg-tm--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s or t)."))
|
||||
((< (length cards) 3) (cg-put g :message "Mark at least three cards (SPC), then m."))
|
||||
((cg-tm--meld g 0 cards)
|
||||
(cg-put g :marks nil) (cg-tm--clamp-cursor g)
|
||||
(cg-put g :message "Melded. Lay off with l, meld more, or discard (RET)."))
|
||||
(t (cg-put g :message "Those cards are not a valid set or run.")))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-layoff ()
|
||||
"Lay the cursor card (or marked cards) off onto a table meld."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (marks (cg-tm--marked-cards g)))
|
||||
(cond
|
||||
((not (cg-tm--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s or t)."))
|
||||
(t (let ((cards (or marks (list (nth (cg-get g :cursor) (cg-rummy--hand g 0)))))
|
||||
(any nil))
|
||||
(dolist (c cards) (when (and c (cg-tm--layoff g 0 c)) (setq any t)))
|
||||
(cg-put g :marks nil) (cg-tm--clamp-cursor g)
|
||||
(cg-put g :message (if any "Laid off." "That card fits no meld on the table.")))))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-draw-stock ()
|
||||
"Draw the top stock card."
|
||||
(interactive)
|
||||
(let ((g cg-tm--game))
|
||||
(cond
|
||||
((not (cg-tm--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'draw)) (cg-put g :message "You already drew."))
|
||||
((cg-tm--draw g 0)
|
||||
(cg-put g :step 'play) (cg-tm--clamp-cursor g)
|
||||
(cg-put g :message "Meld (m), lay off (l), then discard (RET)."))
|
||||
(t (cg-tm--score-hand g nil)))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-take ()
|
||||
"Take the discard top into your hand."
|
||||
(interactive)
|
||||
(let ((g cg-tm--game))
|
||||
(cond
|
||||
((not (cg-tm--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'draw)) (cg-put g :message "You already drew."))
|
||||
((null (cg-rummy--top g)) (cg-put g :message "The discard pile is empty."))
|
||||
(t (let ((c (cg-tm--take-top g 0)))
|
||||
(cg-put g :step 'play) (cg-tm--clamp-cursor g)
|
||||
(cg-put g :message (format "Took %s. Meld (m), lay off (l), discard (RET)."
|
||||
(cg-rummy-card-string c))))))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-discard ()
|
||||
"Discard the cursor card and end your turn."
|
||||
(interactive)
|
||||
(let* ((g cg-tm--game) (card (nth (cg-get g :cursor) (cg-rummy--hand g 0))))
|
||||
(cond
|
||||
((not (cg-tm--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((eq (cg-get g :step) 'draw) (cg-put g :message "Draw first (s or t)."))
|
||||
((null card) (cg-put g :message "No card selected."))
|
||||
(t (cg-tm--discard g 0 card)
|
||||
(cg-put g :marks nil)
|
||||
(when (eq (cg-get g :phase) 'play)
|
||||
(cg-put g :message "You discarded.")
|
||||
(cg-tm--run g))))
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-new ()
|
||||
"Deal a fresh hand, or a new game when one is over."
|
||||
(interactive)
|
||||
(let ((g cg-tm--game))
|
||||
(when (eq (cg-get g :phase) 'game-over)
|
||||
(cg-put g :scores (make-vector (oref g nplayers) 0)))
|
||||
(cg-put g :reveal nil)
|
||||
(cg-tm--deal g)
|
||||
(cg-tm--run g)
|
||||
(cg-tm--redisplay)))
|
||||
|
||||
(defun cg-tm-redraw () "Redraw the board." (interactive) (cg-tm--redisplay))
|
||||
|
||||
(defun cg-tm-help ()
|
||||
"Describe the table-meld controls."
|
||||
(interactive)
|
||||
(message "Arrows: choose SPC: mark m: meld l: lay off s: draw t: take RET: discard n: new"))
|
||||
|
||||
(defvar cg-tm-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "<left>") #'cg-tm-left)
|
||||
(define-key map (kbd "<right>") #'cg-tm-right)
|
||||
(define-key map (kbd "SPC") #'cg-tm-mark)
|
||||
(define-key map "m" #'cg-tm-meld)
|
||||
(define-key map "l" #'cg-tm-layoff)
|
||||
(define-key map "s" #'cg-tm-draw-stock)
|
||||
(define-key map "t" #'cg-tm-take)
|
||||
(define-key map (kbd "RET") #'cg-tm-discard)
|
||||
(define-key map "n" #'cg-tm-new)
|
||||
(define-key map "g" #'cg-tm-redraw)
|
||||
(define-key map "?" #'cg-tm-help)
|
||||
map)
|
||||
"Keymap for `cg-tm-mode'.")
|
||||
|
||||
(define-derived-mode cg-tm-mode special-mode "Rummy"
|
||||
"Major mode for the table-meld rummy games."
|
||||
(setq-local truncate-lines t))
|
||||
|
||||
(defun cg-tm--start (game buffer-name)
|
||||
"Start GAME in a buffer named BUFFER-NAME."
|
||||
(let ((buf (get-buffer-create buffer-name)))
|
||||
(with-current-buffer buf
|
||||
(cg-tm-mode)
|
||||
(setq cg-tm--game game)
|
||||
(cg-tm--deal game)
|
||||
(cg-tm--run game)
|
||||
(cg-tm--redisplay))
|
||||
(switch-to-buffer buf)))
|
||||
|
||||
;;;; The two games
|
||||
|
||||
(defcustom cg-rummy-basic-players 2
|
||||
"Number of players in Basic Rummy, including you (2-4)."
|
||||
:type '(choice (const 2) (const 3) (const 4)) :group 'card-games)
|
||||
|
||||
(defclass cg-rummy-basic-game (cg-tablemeld-game)
|
||||
((vname :initform "Rummy")
|
||||
(score-style :initform 'go-out)
|
||||
(target :initform 100))
|
||||
"A game of plain Rummy.")
|
||||
|
||||
;;;###autoload
|
||||
(defun cg-rummy-basic ()
|
||||
"Play Basic Rummy against the computer."
|
||||
(interactive)
|
||||
(let ((n (max 2 (min 4 cg-rummy-basic-players))))
|
||||
(cg-tm--start (cg-rummy-basic-game :nplayers n :hand-size (if (= n 2) 10 7))
|
||||
"*Rummy*")))
|
||||
|
||||
(defcustom cg-rum500-players 3
|
||||
"Number of players in Rummy 500, including you (2-4)."
|
||||
:type '(choice (const 2) (const 3) (const 4)) :group 'card-games)
|
||||
|
||||
(defclass cg-rum500-game (cg-tablemeld-game)
|
||||
((vname :initform "Rummy 500")
|
||||
(score-style :initform 'meld-points)
|
||||
(ace-15 :initform t)
|
||||
(ace-high :initform t)
|
||||
(target :initform 500))
|
||||
"A game of Rummy 500.")
|
||||
|
||||
;;;###autoload
|
||||
(defun cg-rum500 ()
|
||||
"Play Rummy 500 against the computer."
|
||||
(interactive)
|
||||
(let ((n (max 2 (min 4 cg-rum500-players))))
|
||||
(cg-tm--start (cg-rum500-game :nplayers n :hand-size (if (= n 2) 13 7))
|
||||
"*Rummy 500*")))
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'cg-rummy-500 #'cg-rum500)
|
||||
|
||||
(provide 'cg-rum500)
|
||||
;;;
|
||||
617
cg-rummy.el
Normal file
617
cg-rummy.el
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
;;; cg-rummy.el --- Rummy meld engine and Gin Rummy -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2026 Corwin Brust
|
||||
|
||||
;; Author: Corwin Brust <corwin@bru.st>
|
||||
;; Maintainer: Corwin Brust <corwin@bru.st>
|
||||
;; Version: 1.0.60
|
||||
;; Package-Requires: ((emacs "26.1"))
|
||||
;; Keywords: games
|
||||
;; URL: https://code.bru.st/corwin/card-game.el
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; The shared meld engine for the rummy family, plus Gin Rummy.
|
||||
;;
|
||||
;; A "meld" is a list of cards that is either a *set* (three or four cards
|
||||
;; of the same rank) or a *run* (three or more cards of the same suit in
|
||||
;; consecutive rank). The engine validates melds, enumerates the melds
|
||||
;; latent in a hand, and -- the hard part -- finds the layout that leaves
|
||||
;; the least deadwood, which drives both knock detection and the AI.
|
||||
;;
|
||||
;; Cards use the package-standard cons (SUIT . RANK) with SUIT 0 spades,
|
||||
;; 1 clubs, 2 diamonds, 3 hearts, and RANK 0 (Ace) .. 12 (King). A joker,
|
||||
;; used only by Hand & Foot, is the cons (joker . 0).
|
||||
;;
|
||||
;; Gin Rummy: a two-handed game of ten-card hands. Draw from the stock or
|
||||
;; take the discard, then discard one card. Knock when your deadwood is
|
||||
;; ten or less, or go gin with none; your opponent then lays off onto your
|
||||
;; melds. First to 100 points wins. This file also provides the abstract
|
||||
;; `cg-rummy-game' base and the rendering helpers reused by the other
|
||||
;; rummy games (cg-rum500.el, cg-handfoot.el).
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'cl-lib)
|
||||
(require 'eieio)
|
||||
(require 'cg-core)
|
||||
|
||||
(defconst cg-rummy-ranks
|
||||
["A" "2" "3" "4" "5" "6" "7" "8" "9" "10" "J" "Q" "K"]
|
||||
"Rank labels indexed 0 (Ace) .. 12 (King).")
|
||||
|
||||
(defface cg-rummy-mark '((t :background "steelblue" :foreground "white"))
|
||||
"Face for a card the player has marked toward a meld."
|
||||
:group 'card-games)
|
||||
|
||||
(defun cg-rummy-card-string (card)
|
||||
"Return a short label for CARD, or a dot for nil."
|
||||
(cond ((null card) "·")
|
||||
((eq (car card) 'joker) (cg-suit-glyph 'joker))
|
||||
(t (concat (aref cg-rummy-ranks (cdr card)) (cg-suit-glyph (car card))))))
|
||||
|
||||
(defun cg-rummy-joker-p (card)
|
||||
"Return non-nil when CARD is a joker."
|
||||
(and card (eq (car card) 'joker)))
|
||||
|
||||
(defun cg-rummy-value (card &optional ace-15)
|
||||
"Return the point value of CARD.
|
||||
Faces and tens are 10, an Ace is 1 (or 15 when ACE-15 is non-nil), and
|
||||
other cards their pip value. A joker is worth 0 here; games that use
|
||||
jokers value them separately."
|
||||
(cond ((or (null card) (cg-rummy-joker-p card)) 0)
|
||||
(t (let ((r (cdr card)))
|
||||
(cond ((= r 0) (if ace-15 15 1))
|
||||
((<= r 8) (1+ r))
|
||||
(t 10))))))
|
||||
|
||||
|
||||
;;;; Meld predicates
|
||||
|
||||
(defun cg-rummy--consec-p (ranks)
|
||||
"Return non-nil when sorted RANKS rise by exactly one with no repeats."
|
||||
(let ((ok t) (prev nil))
|
||||
(dolist (r ranks ok)
|
||||
(when (and prev (/= r (1+ prev))) (setq ok nil))
|
||||
(setq prev r))))
|
||||
|
||||
(cl-defun cg-rummy-set-p (cards &key (min 3) distinct-suits)
|
||||
"Return non-nil when CARDS form a set (>= MIN cards of one rank).
|
||||
With DISTINCT-SUITS, every suit must differ (a single-deck rule)."
|
||||
(and (>= (length cards) min)
|
||||
(not (cl-some #'cg-rummy-joker-p cards))
|
||||
(let ((r (cdr (car cards))))
|
||||
(cl-every (lambda (c) (= (cdr c) r)) cards))
|
||||
(or (not distinct-suits)
|
||||
(let ((suits (mapcar #'car cards)))
|
||||
(= (length suits)
|
||||
(length (delete-dups (copy-sequence suits))))))))
|
||||
|
||||
(cl-defun cg-rummy-run-p (cards &key (min 3) ace-high)
|
||||
"Return non-nil when CARDS form a run (>= MIN cards, one suit, in sequence).
|
||||
With ACE-HIGH, an Ace may sit above the King (Q-K-A)."
|
||||
(and (>= (length cards) min)
|
||||
(not (cl-some #'cg-rummy-joker-p cards))
|
||||
(let ((suit (car (car cards)))
|
||||
(ranks (sort (mapcar #'cdr cards) #'<)))
|
||||
(and (cl-every (lambda (c) (= (car c) suit)) cards)
|
||||
(= (length ranks) (length (delete-dups (copy-sequence ranks))))
|
||||
(or (cg-rummy--consec-p ranks)
|
||||
(and ace-high (eql (car ranks) 0)
|
||||
(cg-rummy--consec-p (sort (cons 13 (cdr ranks)) #'<))))))))
|
||||
|
||||
(cl-defun cg-rummy-meld-p (cards &key (min 3) ace-high distinct-suits)
|
||||
"Return non-nil when CARDS form a valid set or run.
|
||||
MIN, ACE-HIGH and DISTINCT-SUITS are passed through to the predicates."
|
||||
(or (cg-rummy-set-p cards :min min :distinct-suits distinct-suits)
|
||||
(cg-rummy-run-p cards :min min :ace-high ace-high)))
|
||||
|
||||
|
||||
;;;; Candidate melds and best partition
|
||||
|
||||
(defun cg-rummy--combinations (lst k)
|
||||
"Return all K-element combinations of LST, each as a list."
|
||||
(cond ((= k 0) (list nil))
|
||||
((null lst) nil)
|
||||
(t (append
|
||||
(mapcar (lambda (c) (cons (car lst) c))
|
||||
(cg-rummy--combinations (cdr lst) (1- k)))
|
||||
(cg-rummy--combinations (cdr lst) k)))))
|
||||
|
||||
(defun cg-rummy--runs-in (pairs)
|
||||
"Return index-lists of runs (length >= 3) within PAIRS.
|
||||
PAIRS is a list of (RANK . INDEX) sorted ascending by RANK."
|
||||
(let ((res '()) (vec (vconcat pairs)))
|
||||
(let ((n (length vec)))
|
||||
(dotimes (i n)
|
||||
(let ((idxs (list (cdr (aref vec i))))
|
||||
(last (car (aref vec i)))
|
||||
(j (1+ i)))
|
||||
(while (and (< j n) (= (car (aref vec j)) (1+ last)))
|
||||
(setq last (car (aref vec j)))
|
||||
(setq idxs (cons (cdr (aref vec j)) idxs))
|
||||
(when (>= (length idxs) 3)
|
||||
(push (reverse idxs) res))
|
||||
(setq j (1+ j))))))
|
||||
res))
|
||||
|
||||
(cl-defun cg-rummy--candidate-melds (cards &key ace-high)
|
||||
"Return candidate melds for CARDS as a list of index-lists.
|
||||
Each index-list refers to positions in CARDS. With ACE-HIGH, high runs
|
||||
ending in an Ace are also generated."
|
||||
(let* ((vec (vconcat cards)) (n (length vec)) (melds '()))
|
||||
;; sets, grouped by rank
|
||||
(let ((byrank (make-hash-table :test 'eql)))
|
||||
(dotimes (i n)
|
||||
(let ((c (aref vec i)))
|
||||
(unless (cg-rummy-joker-p c)
|
||||
(push i (gethash (cdr c) byrank)))))
|
||||
(maphash
|
||||
(lambda (_r idxs)
|
||||
(when (>= (length idxs) 3)
|
||||
(dolist (k '(3 4))
|
||||
(dolist (combo (cg-rummy--combinations idxs k))
|
||||
(when (cg-rummy-set-p (mapcar (lambda (i) (aref vec i)) combo)
|
||||
:distinct-suits t)
|
||||
(push (sort (copy-sequence combo) #'<) melds))))))
|
||||
byrank))
|
||||
;; runs, grouped by suit
|
||||
(let ((bysuit (make-hash-table :test 'eql)))
|
||||
(dotimes (i n)
|
||||
(let ((c (aref vec i)))
|
||||
(unless (cg-rummy-joker-p c)
|
||||
(push (cons (cdr c) i) (gethash (car c) bysuit)))))
|
||||
(maphash
|
||||
(lambda (_s pairs)
|
||||
(let ((sorted (sort (copy-sequence pairs)
|
||||
(lambda (a b) (< (car a) (car b))))))
|
||||
(dolist (m (cg-rummy--runs-in sorted)) (push (sort m #'<) melds)))
|
||||
(when (and ace-high (assq 0 pairs))
|
||||
(let* ((hi (mapcar (lambda (p) (if (= (car p) 0) (cons 13 (cdr p)) p))
|
||||
pairs))
|
||||
(sh (sort hi (lambda (a b) (< (car a) (car b))))))
|
||||
(dolist (m (cg-rummy--runs-in sh)) (push (sort m #'<) melds)))))
|
||||
bysuit))
|
||||
(delete-dups melds)))
|
||||
|
||||
(cl-defun cg-rummy-best-partition (cards &key ace-high ace-15)
|
||||
"Return the lowest-deadwood layout of CARDS.
|
||||
The result is a plist (:melds MELDS :deadwood CARDS :count N) where MELDS
|
||||
is a list of card-lists, DEADWOOD the unmelded cards and N their value
|
||||
sum. ACE-HIGH allows Q-K-A runs; ACE-15 scores Aces at 15."
|
||||
(let* ((vec (vconcat cards)) (n (length vec))
|
||||
(cand (cg-rummy--candidate-melds cards :ace-high ace-high))
|
||||
(mmasks (mapcar (lambda (m)
|
||||
(let ((b 0)) (dolist (i m) (setq b (logior b (ash 1 i))))
|
||||
b))
|
||||
cand))
|
||||
(memo (make-hash-table :test 'eql))
|
||||
(full (1- (ash 1 n))))
|
||||
(cl-labels
|
||||
((cval (i) (cg-rummy-value (aref vec i) ace-15))
|
||||
(lowbit (avail)
|
||||
(let ((i 0)) (while (zerop (logand avail (ash 1 i))) (setq i (1+ i))) i))
|
||||
(solve (avail)
|
||||
(if (zerop avail) (cons 0 '())
|
||||
(or (gethash avail memo)
|
||||
(let* ((i (lowbit avail))
|
||||
(ibit (ash 1 i))
|
||||
(sub0 (solve (logxor avail ibit)))
|
||||
(best (cons (+ (cval i) (car sub0)) (cdr sub0))))
|
||||
(dolist (mask mmasks)
|
||||
(when (and (/= 0 (logand mask ibit))
|
||||
(= mask (logand mask avail)))
|
||||
(let ((sub (solve (logand avail (lognot mask)))))
|
||||
(when (< (car sub) (car best))
|
||||
(setq best (cons (car sub) (cons mask (cdr sub))))))))
|
||||
(puthash avail best memo)
|
||||
best)))))
|
||||
(let* ((res (solve full)) (maskmelds (cdr res)) (used 0))
|
||||
(dolist (m maskmelds) (setq used (logior used m)))
|
||||
(list :melds
|
||||
(mapcar (lambda (mask)
|
||||
(let ((cl '()))
|
||||
(dotimes (i n)
|
||||
(when (/= 0 (logand mask (ash 1 i)))
|
||||
(push (aref vec i) cl)))
|
||||
(nreverse cl)))
|
||||
maskmelds)
|
||||
:deadwood
|
||||
(let ((dl '()))
|
||||
(dotimes (i n)
|
||||
(when (= 0 (logand used (ash 1 i))) (push (aref vec i) dl)))
|
||||
(nreverse dl))
|
||||
:count (car res))))))
|
||||
|
||||
(defun cg-rummy-deadwood (cards &optional ace-high ace-15)
|
||||
"Return the minimum deadwood value of CARDS.
|
||||
ACE-HIGH and ACE-15 are passed to `cg-rummy-best-partition'."
|
||||
(plist-get (cg-rummy-best-partition cards :ace-high ace-high :ace-15 ace-15)
|
||||
:count))
|
||||
|
||||
(defun cg-rummy-layoff-p (card melds &optional ace-high)
|
||||
"Return the first meld in MELDS that CARD extends, or nil.
|
||||
ACE-HIGH allows extending a run with a high Ace."
|
||||
(cl-find-if (lambda (m)
|
||||
(cg-rummy-meld-p (cons card m) :min 3 :ace-high ace-high))
|
||||
melds))
|
||||
|
||||
|
||||
;;;; Shared deck and base game
|
||||
|
||||
(defun cg-rummy-deck (&optional ndecks jokers)
|
||||
"Return a shuffled deck of NDECKS standard packs plus JOKERS jokers each.
|
||||
NDECKS defaults to 1 and JOKERS to 0."
|
||||
(random t)
|
||||
(let ((cards '()))
|
||||
(dotimes (_ (or ndecks 1))
|
||||
(dotimes (s 4) (dotimes (r 13) (push (cons s r) cards)))
|
||||
(dotimes (_ (or jokers 0)) (push (cons 'joker 0) cards)))
|
||||
(cg-shuffle cards)))
|
||||
|
||||
(defclass cg-rummy-game (cg-game) ()
|
||||
"Abstract base for rummy-style draw-and-discard games."
|
||||
:abstract t)
|
||||
|
||||
(defsubst cg-rummy--hand (game s) (aref (cg-get game :hands) s))
|
||||
(defsubst cg-rummy--set-hand (game s v) (aset (cg-get game :hands) s v))
|
||||
(defsubst cg-rummy--top (game) (car (cg-get game :discard)))
|
||||
|
||||
(defun cg-rummy-sort-hand (cards)
|
||||
"Return CARDS sorted by suit then rank for display, jokers last."
|
||||
(sort (copy-sequence cards)
|
||||
(lambda (a b)
|
||||
(let ((sa (if (cg-rummy-joker-p a) 99 (car a)))
|
||||
(sb (if (cg-rummy-joker-p b) 99 (car b))))
|
||||
(if (= sa sb) (< (cdr a) (cdr b)) (< sa sb))))))
|
||||
|
||||
(defun cg-rummy--render-cards (cards cursor marks &optional hint-fn)
|
||||
"Return a propertized row of CARDS.
|
||||
CURSOR is the highlighted index, MARKS a list of marked indices, and
|
||||
HINT-FN an optional predicate marking playable cards."
|
||||
(let ((i 0) (out '()))
|
||||
(dolist (c cards)
|
||||
(let ((cs (cg-rummy-card-string c)) (faces nil))
|
||||
(when (and (not (cg-rummy-joker-p c)) (cg-red-suit-p (car c)))
|
||||
(push 'cg-red-suit faces))
|
||||
(when (and hint-fn (funcall hint-fn c)) (push 'cg-hint faces))
|
||||
(when (memq i marks) (push 'cg-rummy-mark faces))
|
||||
(when (eql i cursor) (push 'cg-cursor faces))
|
||||
(push (propertize (format "%4s" cs) 'face (or faces 'default)) out))
|
||||
(setq i (1+ i)))
|
||||
(apply #'concat (nreverse out))))
|
||||
|
||||
(defun cg-rummy--melds-string (melds)
|
||||
"Return a one-line depiction of MELDS (a list of card-lists)."
|
||||
(mapconcat (lambda (m) (mapconcat #'cg-rummy-card-string m " "))
|
||||
melds " | "))
|
||||
|
||||
|
||||
;;;; Gin Rummy
|
||||
|
||||
(defcustom cg-gin-target 100
|
||||
"Points needed to win a game of Gin Rummy."
|
||||
:type 'integer :group 'card-games)
|
||||
|
||||
(defclass cg-gin-game (cg-rummy-game)
|
||||
((vname :initform "Gin Rummy"))
|
||||
"A two-handed game of Gin Rummy.")
|
||||
|
||||
(cl-defmethod cg-gin--deal ((game cg-gin-game))
|
||||
"Deal a fresh Gin hand into GAME."
|
||||
(let ((deck (cg-rummy-deck)) (hands (make-vector 2 nil)))
|
||||
(dotimes (s 2)
|
||||
(aset hands s (cg-rummy-sort-hand (cl-loop repeat 10 collect (pop deck)))))
|
||||
(cg-put game :hands hands)
|
||||
(cg-put game :discard (list (pop deck)))
|
||||
(cg-put game :stock deck)
|
||||
(cg-put game :turn 0)
|
||||
(cg-put game :step 'draw)
|
||||
(cg-put game :phase 'play)
|
||||
(cg-put game :cursor 0)
|
||||
(unless (cg-get game :scores) (cg-put game :scores (make-vector 2 0)))
|
||||
(cg-put game :message
|
||||
"Your turn: s draws from stock, t takes the discard.")
|
||||
game))
|
||||
|
||||
(defun cg-gin--deadwood (cards) (cg-rummy-deadwood cards))
|
||||
|
||||
(defun cg-gin--draw (game s)
|
||||
"Move one stock card to seat S's hand; return it or nil if stock empty."
|
||||
(let ((stock (cg-get game :stock)))
|
||||
(when stock
|
||||
(let ((c (pop stock)))
|
||||
(cg-put game :stock stock)
|
||||
(cg-rummy--set-hand game s (cons c (cg-rummy--hand game s)))
|
||||
c))))
|
||||
|
||||
(defun cg-gin--take (game s)
|
||||
"Move the discard top to seat S's hand and return it."
|
||||
(let ((c (cg-rummy--top game)))
|
||||
(cg-put game :discard (cdr (cg-get game :discard)))
|
||||
(cg-rummy--set-hand game s (cons c (cg-rummy--hand game s)))
|
||||
c))
|
||||
|
||||
(defun cg-gin--discard (game s card)
|
||||
"Discard CARD from seat S's hand onto the pile."
|
||||
(cg-rummy--set-hand game s (cg-rummy-sort-hand
|
||||
(remove card (cg-rummy--hand game s))))
|
||||
(cg-put game :discard (cons card (cg-get game :discard))))
|
||||
|
||||
(defun cg-gin--ai-best-discard (game s)
|
||||
"Return the card seat S should discard to minimise its deadwood."
|
||||
(let ((hand (cg-rummy--hand game s)) (best nil) (bestv most-positive-fixnum))
|
||||
(dolist (c hand best)
|
||||
(let ((d (cg-gin--deadwood (remove c hand))))
|
||||
(when (or (< d bestv)
|
||||
(and (= d bestv) best
|
||||
(> (cg-rummy-value c) (cg-rummy-value best))))
|
||||
(setq best c bestv d))))))
|
||||
|
||||
(cl-defmethod cg-gin--ai-turn ((game cg-gin-game) s)
|
||||
"Play seat S's whole turn, then hand control back."
|
||||
(let* ((hand (cg-rummy--hand game s))
|
||||
(cur (cg-gin--deadwood hand))
|
||||
(up (cg-rummy--top game))
|
||||
(with (and up (cg-gin--deadwood (cons up hand))))
|
||||
;; take the discard only when it strictly helps
|
||||
(drew (if (and up with (< with cur))
|
||||
(cg-gin--take game s)
|
||||
(cg-gin--draw game s))))
|
||||
(if (not drew)
|
||||
(cg-gin--exhaust game) ; stock exhausted: wash the hand
|
||||
(let ((card (cg-gin--ai-best-discard game s)))
|
||||
(cg-gin--discard game s card)
|
||||
(let ((dw (cg-gin--deadwood (cg-rummy--hand game s))))
|
||||
(if (<= dw 10)
|
||||
(cg-gin--knock game s)
|
||||
(cg-put game :turn 0)
|
||||
(cg-put game :step 'draw)
|
||||
(cg-put game :message
|
||||
(format "Opponent discarded %s. s draws, t takes."
|
||||
(cg-rummy-card-string card)))))))))
|
||||
|
||||
(cl-defmethod cg-gin--knock ((game cg-gin-game) knocker)
|
||||
"Resolve the hand when KNOCKER knocks; score and end the hand."
|
||||
(let* ((kpart (cg-rummy-best-partition (cg-rummy--hand game knocker)))
|
||||
(kmelds (plist-get kpart :melds))
|
||||
(kdw (plist-get kpart :count))
|
||||
(opp (- 1 knocker))
|
||||
(opart (cg-rummy-best-partition (cg-rummy--hand game opp)))
|
||||
(odead (plist-get opart :deadwood))
|
||||
(gin (= kdw 0)))
|
||||
;; opponent lays off onto the knocker's melds (not allowed on gin)
|
||||
(unless gin
|
||||
(let (remaining)
|
||||
(dolist (c odead)
|
||||
(let ((m (cg-rummy-layoff-p c kmelds)))
|
||||
(if m (setcdr (last m) (list c)) ; extend that meld in place
|
||||
(push c remaining))))
|
||||
(setq odead (nreverse remaining))))
|
||||
(let* ((odw (apply #'+ (mapcar #'cg-rummy-value odead)))
|
||||
(scores (cg-get game :scores))
|
||||
(winner knocker) (pts 0) (note ""))
|
||||
(cond
|
||||
(gin (setq pts (+ 25 odw) note "Gin!"))
|
||||
((< kdw odw) (setq pts (- odw kdw) note "Knock."))
|
||||
(t ;; undercut: defender wins
|
||||
(setq winner opp pts (+ 25 (- kdw odw)) note "Undercut!")))
|
||||
(aset scores winner (+ (aref scores winner) pts))
|
||||
(cg-put game :phase 'hand-over)
|
||||
(cg-put game :reveal t)
|
||||
(cg-put game :winner winner)
|
||||
(cg-put game :message
|
||||
(format "%s %s scores %d. %s (n: next hand)"
|
||||
note
|
||||
(if (= winner 0) "You" "Opponent") pts
|
||||
(if (>= (aref scores winner) cg-gin-target)
|
||||
(progn (cg-put game :phase 'game-over)
|
||||
(format "%s wins the game!"
|
||||
(if (= winner 0) "You" "Opponent")))
|
||||
(format "Score %d-%d."
|
||||
(aref scores 0) (aref scores 1))))))))
|
||||
|
||||
(cl-defmethod cg-gin--exhaust ((game cg-gin-game))
|
||||
"End a hand washed out because the stock ran dry."
|
||||
(cg-put game :phase 'hand-over)
|
||||
(cg-put game :reveal t)
|
||||
(cg-put game :message "Stock exhausted -- the hand is a wash. n: next hand."))
|
||||
|
||||
;;;; Gin UI
|
||||
|
||||
(defvar-local cg-gin--game nil "The Gin Rummy game in the current buffer.")
|
||||
|
||||
(cl-defmethod cg-render ((game cg-gin-game))
|
||||
"Return a propertized depiction of the Gin GAME for a text display."
|
||||
(let* ((out '()) (scores (cg-get game :scores))
|
||||
(reveal (cg-get game :reveal))
|
||||
(hand (cg-rummy--hand game 0)) (cursor (cg-get game :cursor)))
|
||||
(push (format " Gin Rummy first to %d\n\n" cg-gin-target) out)
|
||||
(push (format " Opponent: %d cards score %d\n"
|
||||
(length (cg-rummy--hand game 1)) (aref scores 1)) out)
|
||||
(when reveal
|
||||
(let ((p (cg-rummy-best-partition (cg-rummy--hand game 1))))
|
||||
(push (format " melds: %s\n deadwood: %s (%d)\n"
|
||||
(cg-rummy--melds-string (plist-get p :melds))
|
||||
(mapconcat #'cg-rummy-card-string (plist-get p :deadwood) " ")
|
||||
(plist-get p :count))
|
||||
out)))
|
||||
(push (format "\n Discard: %s Stock: %d\n\n"
|
||||
(let ((cs (cg-rummy-card-string (cg-rummy--top game))))
|
||||
(if (and (cg-rummy--top game) (cg-red-suit-p (car (cg-rummy--top game))))
|
||||
(propertize cs 'face 'cg-red-suit) cs))
|
||||
(length (cg-get game :stock)))
|
||||
out)
|
||||
(push (format " Your hand (deadwood %d, score %d):\n "
|
||||
(cg-gin--deadwood hand) (aref scores 0)) out)
|
||||
(push (cg-rummy--render-cards hand cursor nil) out)
|
||||
(when reveal
|
||||
(let ((p (cg-rummy-best-partition hand)))
|
||||
(push (format "\n melds: %s\n deadwood: %s"
|
||||
(cg-rummy--melds-string (plist-get p :melds))
|
||||
(mapconcat #'cg-rummy-card-string (plist-get p :deadwood) " "))
|
||||
out)))
|
||||
(push (format "\n\n %s\n" (cg-get game :message)) out)
|
||||
(apply #'concat (nreverse out))))
|
||||
|
||||
(defun cg-gin--redisplay ()
|
||||
"Redraw the Gin Rummy buffer."
|
||||
(let ((game cg-gin--game) (inhibit-read-only t))
|
||||
(setq-local mode-line-process
|
||||
(format " [%s]" (or (cg-get game :step) (cg-get game :phase))))
|
||||
(erase-buffer) (insert (cg-render game)) (goto-char (point-min))))
|
||||
|
||||
(defun cg-gin--cursor-card ()
|
||||
"Return the card under the cursor in your hand."
|
||||
(nth (cg-get cg-gin--game :cursor) (cg-rummy--hand cg-gin--game 0)))
|
||||
|
||||
(defun cg-gin-left ()
|
||||
"Move the hand cursor left."
|
||||
(interactive)
|
||||
(let* ((g cg-gin--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1- (cg-get g :cursor)) n)))
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin-right ()
|
||||
"Move the hand cursor right."
|
||||
(interactive)
|
||||
(let* ((g cg-gin--game) (n (length (cg-rummy--hand g 0))))
|
||||
(when (> n 0) (cg-put g :cursor (mod (1+ (cg-get g :cursor)) n)))
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin--my-turn-p (g)
|
||||
"Return non-nil when it is your turn to act in G."
|
||||
(and (eq (cg-get g :phase) 'play) (= (cg-get g :turn) 0)))
|
||||
|
||||
(defun cg-gin-draw-stock ()
|
||||
"Draw the top stock card into your hand."
|
||||
(interactive)
|
||||
(let ((g cg-gin--game))
|
||||
(cond
|
||||
((not (cg-gin--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'draw)) (cg-put g :message "Discard first (RET)."))
|
||||
((cg-gin--draw g 0)
|
||||
(cg-put g :step 'discard)
|
||||
(cg-put g :cursor 0)
|
||||
(cg-put g :message "Choose a card and discard with RET, or knock with k."))
|
||||
(t (cg-gin--exhaust g)))
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin-take-discard ()
|
||||
"Take the discard top into your hand."
|
||||
(interactive)
|
||||
(let ((g cg-gin--game))
|
||||
(cond
|
||||
((not (cg-gin--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'draw)) (cg-put g :message "Discard first (RET)."))
|
||||
((null (cg-rummy--top g)) (cg-put g :message "The discard pile is empty."))
|
||||
(t (let ((c (cg-gin--take g 0)))
|
||||
(cg-put g :step 'discard)
|
||||
(cg-put g :cursor 0)
|
||||
(cg-put g :message
|
||||
(format "Took %s. Discard with RET, or knock with k."
|
||||
(cg-rummy-card-string c))))))
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin--after-discard (g)
|
||||
"Hand control to the opponent after you discard in G."
|
||||
(cg-put g :turn 1)
|
||||
(cg-put g :step 'draw)
|
||||
(cg-gin--ai-turn g 1)
|
||||
(cg-gin--redisplay))
|
||||
|
||||
(defun cg-gin-discard ()
|
||||
"Discard the selected card and end your turn."
|
||||
(interactive)
|
||||
(let* ((g cg-gin--game) (card (cg-gin--cursor-card)))
|
||||
(cond
|
||||
((not (cg-gin--my-turn-p g)) (cg-put g :message "Not your turn.")
|
||||
(cg-gin--redisplay))
|
||||
((not (eq (cg-get g :step) 'discard))
|
||||
(cg-put g :message "Draw first (s or t).") (cg-gin--redisplay))
|
||||
((null card) (cg-put g :message "No card selected.") (cg-gin--redisplay))
|
||||
(t (cg-gin--discard g 0 card)
|
||||
(cg-put g :message (format "You discarded %s." (cg-rummy-card-string card)))
|
||||
(cg-gin--after-discard g)))))
|
||||
|
||||
(defun cg-gin-knock ()
|
||||
"Knock, discarding the selected card, if your deadwood allows."
|
||||
(interactive)
|
||||
(let* ((g cg-gin--game) (card (cg-gin--cursor-card)))
|
||||
(cond
|
||||
((not (cg-gin--my-turn-p g)) (cg-put g :message "Not your turn."))
|
||||
((not (eq (cg-get g :step) 'discard))
|
||||
(cg-put g :message "Draw first (s or t)."))
|
||||
((null card) (cg-put g :message "No card selected."))
|
||||
(t (let ((dw (cg-gin--deadwood (remove card (cg-rummy--hand g 0)))))
|
||||
(if (> dw 10)
|
||||
(cg-put g :message
|
||||
(format "Can't knock: that leaves %d deadwood (need <= 10)." dw))
|
||||
(cg-gin--discard g 0 card)
|
||||
(cg-gin--knock g 0)))))
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin-new ()
|
||||
"Deal a fresh hand (or a new game when one is over)."
|
||||
(interactive)
|
||||
(let ((g cg-gin--game))
|
||||
(when (eq (cg-get g :phase) 'game-over) (cg-put g :scores (make-vector 2 0)))
|
||||
(cg-put g :reveal nil)
|
||||
(cg-gin--deal g)
|
||||
(cg-gin--redisplay)))
|
||||
|
||||
(defun cg-gin-redraw () "Redraw the board." (interactive) (cg-gin--redisplay))
|
||||
|
||||
(defun cg-gin-help ()
|
||||
"Describe the Gin Rummy controls."
|
||||
(interactive)
|
||||
(message "Arrows: choose s: draw stock t: take discard RET: discard k: knock n: new g: redraw"))
|
||||
|
||||
(defvar cg-gin-mode-map
|
||||
(let ((map (make-sparse-keymap)))
|
||||
(define-key map (kbd "<left>") #'cg-gin-left)
|
||||
(define-key map (kbd "<right>") #'cg-gin-right)
|
||||
(define-key map "s" #'cg-gin-draw-stock)
|
||||
(define-key map "t" #'cg-gin-take-discard)
|
||||
(define-key map (kbd "RET") #'cg-gin-discard)
|
||||
(define-key map "k" #'cg-gin-knock)
|
||||
(define-key map "n" #'cg-gin-new)
|
||||
(define-key map "g" #'cg-gin-redraw)
|
||||
(define-key map "?" #'cg-gin-help)
|
||||
map)
|
||||
"Keymap for `cg-gin-mode'.")
|
||||
|
||||
(define-derived-mode cg-gin-mode special-mode "Gin"
|
||||
"Major mode for Gin Rummy."
|
||||
(setq-local truncate-lines t))
|
||||
|
||||
;;;###autoload
|
||||
(defun cg-gin ()
|
||||
"Play Gin Rummy against the computer."
|
||||
(interactive)
|
||||
(let ((buf (get-buffer-create "*Gin Rummy*")))
|
||||
(with-current-buffer buf
|
||||
(cg-gin-mode)
|
||||
(setq cg-gin--game (cg-gin-game))
|
||||
(cg-gin--deal cg-gin--game)
|
||||
(cg-gin--redisplay))
|
||||
(switch-to-buffer buf)))
|
||||
|
||||
;;;###autoload
|
||||
(defalias 'cg-gin-rummy #'cg-gin)
|
||||
|
||||
(provide 'cg-rummy)
|
||||
;;; cg-rummy.el ends here
|
||||
4
runemacs.sh
Normal file
4
runemacs.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
# Load the card-games package from this directory and open the launcher.
|
||||
dir="$(cd "$(dirname "$0")" && pwd)"
|
||||
exec emacs -L "$dir" -l card-games -f card-game "$@"
|
||||
|
|
@ -828,3 +828,83 @@
|
|||
|
||||
(ert-deftest cgt-pres-render ()
|
||||
(let ((g (cg-pres--deal (cg-president-game)))) (should (stringp (cg-render g)))))
|
||||
;;;; Rummy family
|
||||
|
||||
(ert-deftest cgt-rummy-set-run ()
|
||||
(should (cg-rummy-set-p '((0 . 5) (1 . 5) (2 . 5)) :distinct-suits t))
|
||||
(should-not (cg-rummy-set-p '((0 . 5) (0 . 5) (2 . 5)) :distinct-suits t))
|
||||
(should (cg-rummy-run-p '((0 . 3) (0 . 4) (0 . 5))))
|
||||
(should-not (cg-rummy-run-p '((0 . 3) (1 . 4) (0 . 5))))
|
||||
(should (cg-rummy-run-p '((0 . 10) (0 . 11) (0 . 12) (0 . 0)) :ace-high t))
|
||||
(should-not (cg-rummy-run-p '((0 . 11) (0 . 12) (0 . 0))))
|
||||
(should (cg-rummy-run-p '((0 . 0) (0 . 1) (0 . 2)))))
|
||||
|
||||
(ert-deftest cgt-rummy-best-partition ()
|
||||
(let* ((hand '((0 . 2)(0 . 3)(0 . 4) (0 . 6)(1 . 6)(2 . 6)
|
||||
(3 . 8)(3 . 9)(3 . 10) (3 . 12)))
|
||||
(p (cg-rummy-best-partition hand)))
|
||||
(should (= 3 (length (plist-get p :melds))))
|
||||
(should (= 10 (plist-get p :count))))
|
||||
(should (= 0 (cg-rummy-deadwood
|
||||
'((0 . 0)(0 . 1)(0 . 2) (1 . 4)(1 . 5)(1 . 6)
|
||||
(2 . 8)(2 . 9)(2 . 10)(2 . 11))))))
|
||||
|
||||
(ert-deftest cgt-rummy-layoff ()
|
||||
(should (cg-rummy-layoff-p '(3 . 11) '(((3 . 8)(3 . 9)(3 . 10)))))
|
||||
(should-not (cg-rummy-layoff-p '(0 . 4) '(((3 . 8)(3 . 9)(3 . 10))))))
|
||||
|
||||
(ert-deftest cgt-gin-full-game ()
|
||||
(let ((g (cg-gin-game)) (turns 0))
|
||||
(cg-gin--deal g)
|
||||
(while (and (not (eq (cg-get g :phase) 'game-over)) (< turns 100000))
|
||||
(if (eq (cg-get g :phase) 'hand-over)
|
||||
(progn (cg-put g :reveal nil) (cg-gin--deal g))
|
||||
(cg-gin--ai-turn g (cg-get g :turn)) (cl-incf turns)))
|
||||
(should (eq (cg-get g :phase) 'game-over))
|
||||
(should (>= (apply #'max (append (cg-get g :scores) nil)) cg-gin-target))))
|
||||
|
||||
(ert-deftest cgt-gin-render () (let ((g (cg-gin--deal (cg-gin-game))))
|
||||
(should (stringp (cg-render g)))))
|
||||
|
||||
(ert-deftest cgt-tablemeld-games ()
|
||||
(dolist (mk (list (lambda () (cg-rummy-basic-game :nplayers 3 :hand-size 7))
|
||||
(lambda () (cg-rum500-game :nplayers 3 :hand-size 7))))
|
||||
(let ((g (funcall mk)) (turns 0))
|
||||
(cg-tm--deal g)
|
||||
(while (and (not (eq (cg-get g :phase) 'game-over)) (< turns 200000))
|
||||
(if (eq (cg-get g :phase) 'hand-over)
|
||||
(progn (cg-put g :reveal nil) (cg-tm--deal g))
|
||||
(cg-tm--ai-turn g (cg-get g :turn)) (cl-incf turns)))
|
||||
(should (eq (cg-get g :phase) 'game-over))
|
||||
(should (stringp (cg-render g))))))
|
||||
|
||||
(ert-deftest cgt-tablemeld-conservation ()
|
||||
(let ((g (cg-rum500-game :nplayers 3 :hand-size 7)))
|
||||
(cg-tm--deal g)
|
||||
(dotimes (_ 30) (when (eq (cg-get g :phase) 'play)
|
||||
(cg-tm--ai-turn g (cg-get g :turn))))
|
||||
(let ((tot (+ (length (cg-get g :stock)) (length (cg-get g :discard))
|
||||
(apply #'+ (mapcar (lambda (r) (length (cdr r))) (cg-get g :table))))))
|
||||
(dotimes (s 3) (setq tot (+ tot (length (cg-rummy--hand g s)))))
|
||||
(should (= tot 52)))))
|
||||
|
||||
(ert-deftest cgt-handfoot-book ()
|
||||
(should (cg-hf--book-valid-p '((0 . 7)(1 . 7)(2 . 7))))
|
||||
(should (cg-hf--book-valid-p '((0 . 7)(1 . 7)(joker . 0)))) ; dirty
|
||||
(should-not (cg-hf--book-valid-p '((0 . 7)(joker . 0)(joker . 0)))) ; wilds > nats
|
||||
(should-not (cg-hf--book-valid-p '((0 . 2)(1 . 2)(2 . 2)))) ; threes
|
||||
(should (cg-hf--book-complete-p '(a b c d e f g)))
|
||||
(should (cg-hf--book-clean-p '((0 . 7)(1 . 7)(2 . 7))))
|
||||
(should-not (cg-hf--book-clean-p '((0 . 7)(1 . 7)(joker . 0)))))
|
||||
|
||||
(ert-deftest cgt-handfoot-full-game ()
|
||||
(let ((g (cg-handfoot-game)) (rounds 0) (turns 0) (expect (* 5 54)))
|
||||
(cg-put g :nplayers 4) (cg-put g :nteams 2) (cg-put g :scores (make-vector 2 0))
|
||||
(cg-hf--deal g)
|
||||
(while (and (not (eq (cg-get g :phase) 'game-over)) (< rounds 60))
|
||||
(cond ((eq (cg-get g :phase) 'round-over)
|
||||
(cg-put g :reveal nil) (cg-hf--deal g) (cl-incf rounds))
|
||||
(t (cg-hf--ai-turn g (cg-get g :turn)) (cl-incf turns)
|
||||
(when (> turns 500000) (error "runaway")))))
|
||||
(should (eq (cg-get g :phase) 'game-over))
|
||||
(should (stringp (cg-render g)))))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue