Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
;;; cg-trick.el --- Four-hand trick-taking games (Hearts, Spades) -*- 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:
|
|
|
|
|
|
|
|
|
|
;; A small four-handed trick-taking engine and two games built on it:
|
|
|
|
|
;;
|
|
|
|
|
;; `cg-hearts' -- the classic avoidance game; dodge every heart and the
|
|
|
|
|
;; Queen of Spades, or take them all to "shoot the moon".
|
|
|
|
|
;; `cg-spades' -- partnership bidding; spades are always trump; make your
|
|
|
|
|
;; side's combined bid, beware of bags, and dare a nil.
|
|
|
|
|
;;
|
|
|
|
|
;; You sit South (seat 0); the other three seats are played by simple but
|
|
|
|
|
;; legal AI. Cards are the package-standard cons (SUIT . RANK) with SUIT
|
|
|
|
|
;; 0 spades, 1 clubs, 2 diamonds, 3 hearts and RANK 0 (the Two) .. 12 (the
|
|
|
|
|
;; Ace); within a suit the higher rank wins, with the trump suit beating
|
|
|
|
|
;; every plain suit.
|
|
|
|
|
|
|
|
|
|
;;; Code:
|
|
|
|
|
|
|
|
|
|
(require 'cl-lib)
|
|
|
|
|
(require 'eieio)
|
|
|
|
|
(require 'cg-core)
|
2026-06-25 07:20:03 -05:00
|
|
|
(require 'cg-svg)
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
|
|
|
|
|
;;;; Cards
|
|
|
|
|
|
|
|
|
|
(defconst cg-trick-ranks
|
|
|
|
|
["2" "3" "4" "5" "6" "7" "8" "9" "10" "J" "Q" "K" "A"]
|
|
|
|
|
"Rank labels indexed 0 (Two) .. 12 (Ace).")
|
|
|
|
|
|
|
|
|
|
(defconst cg-trick-seat-names ["South" "West" "North" "East"]
|
|
|
|
|
"Seat names indexed 0..3, going clockwise from the human player.")
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-card-string (card)
|
|
|
|
|
"Return a short string for CARD."
|
|
|
|
|
(if (null card) "·"
|
|
|
|
|
(concat (aref cg-trick-ranks (cdr card)) (cg-suit-glyph (car card)))))
|
|
|
|
|
|
|
|
|
|
(defsubst cg-trick-red-p (card) (and card (cg-red-suit-p (car card))))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--full-deck ()
|
|
|
|
|
"Return a fresh shuffled 52-card deck."
|
|
|
|
|
(random t)
|
|
|
|
|
(cg-shuffle (cl-loop for s below 4 append
|
|
|
|
|
(cl-loop for r below 13 collect (cons s r)))))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--sort (cards)
|
|
|
|
|
"Return CARDS sorted by suit then rank for display."
|
|
|
|
|
(sort (copy-sequence cards)
|
|
|
|
|
(lambda (a b) (if (= (car a) (car b)) (< (cdr a) (cdr b))
|
|
|
|
|
(< (car a) (car b))))))
|
|
|
|
|
|
|
|
|
|
;;;; Classes
|
|
|
|
|
|
|
|
|
|
(defclass cg-trick-game (cg-game)
|
|
|
|
|
((trump :initform nil :documentation "Trump suit index, or nil for none.")
|
|
|
|
|
(restricted :initform 3 :documentation "Suit that cannot be led until broken.")
|
|
|
|
|
(target :initform 100 :documentation "Score that ends the game.")
|
|
|
|
|
(hand-size :initform 13 :documentation "Cards dealt to each seat per hand.")
|
|
|
|
|
(vname :initform "Trick game"))
|
|
|
|
|
"Abstract base for four-handed trick-taking games."
|
|
|
|
|
:abstract t)
|
|
|
|
|
|
|
|
|
|
(defclass cg-hearts-game (cg-trick-game)
|
|
|
|
|
((trump :initform nil) (restricted :initform 3) (target :initform 100)
|
|
|
|
|
(vname :initform "Hearts"))
|
|
|
|
|
"Hearts: no trump; avoid hearts and the Queen of Spades.")
|
|
|
|
|
|
|
|
|
|
(defclass cg-spades-game (cg-trick-game)
|
|
|
|
|
((trump :initform 0) (restricted :initform 0) (target :initform 500)
|
|
|
|
|
(vname :initform "Spades"))
|
|
|
|
|
"Spades: spades are trump; partnership bidding to 500.")
|
|
|
|
|
|
|
|
|
|
;;;; Dealing
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--deal ((game cg-trick-game))
|
|
|
|
|
"Deal a fresh hand into GAME."
|
|
|
|
|
(let ((deck (cg-trick--full-deck))
|
|
|
|
|
(hands (make-vector 4 nil))
|
|
|
|
|
(hs (oref game hand-size))
|
|
|
|
|
(last nil))
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(let ((h nil))
|
|
|
|
|
(dotimes (_ hs) (setq last (pop deck)) (push last h))
|
|
|
|
|
(aset hands s (cg-trick--sort h))))
|
|
|
|
|
(cg-put game :hands hands)
|
|
|
|
|
(cg-put game :deck deck)
|
|
|
|
|
(cg-put game :last-card last)
|
|
|
|
|
(cg-put game :trick nil)
|
|
|
|
|
(cg-put game :tricks (make-vector 4 0))
|
|
|
|
|
(cg-put game :taken (make-vector 4 nil))
|
|
|
|
|
(cg-put game :broken nil)
|
|
|
|
|
(cg-put game :trick-no 0)
|
|
|
|
|
game))
|
|
|
|
|
|
|
|
|
|
(defsubst cg-trick--hand (game s) (aref (cg-get game :hands) s))
|
|
|
|
|
(defsubst cg-trick--set-hand (game s v) (aset (cg-get game :hands) s v))
|
|
|
|
|
(defsubst cg-trick--partner (s) (mod (+ s 2) 4))
|
|
|
|
|
(defsubst cg-trick--team (s) (mod s 2))
|
|
|
|
|
|
|
|
|
|
;;;; Trick mechanics
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--led-suit (game)
|
|
|
|
|
"Return the suit led to the current trick, or nil if none yet."
|
|
|
|
|
(let ((tr (cg-get game :trick)))
|
|
|
|
|
(and tr (car (cdr (car (last tr))))))) ; first entry played
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--first-play (game)
|
|
|
|
|
"Return the (SEAT . CARD) led to the current trick, or nil."
|
|
|
|
|
(car (last (cg-get game :trick))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--has-points-only-p ((_ cg-trick-game) _hand) nil)
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--legal-p ((game cg-trick-game) seat card)
|
|
|
|
|
"Return non-nil when SEAT may legally play CARD now."
|
|
|
|
|
(let* ((hand (cg-trick--hand game seat))
|
|
|
|
|
(trick (cg-get game :trick))
|
|
|
|
|
(restricted (oref game restricted))
|
|
|
|
|
(broken (cg-get game :broken)))
|
|
|
|
|
(and (member card hand)
|
|
|
|
|
(if trick
|
|
|
|
|
;; following: must follow the led suit if able
|
|
|
|
|
(let ((led (cg-trick--led-suit game)))
|
|
|
|
|
(if (cl-some (lambda (c) (= (car c) led)) hand)
|
|
|
|
|
(= (car card) led)
|
|
|
|
|
t))
|
|
|
|
|
;; leading: cannot lead the restricted suit until broken,
|
|
|
|
|
;; unless the hand holds nothing else
|
|
|
|
|
(if (and (= (car card) restricted) (not broken))
|
|
|
|
|
(cl-every (lambda (c) (= (car c) restricted)) hand)
|
|
|
|
|
t)))))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--legal-moves (game seat)
|
|
|
|
|
"Return the list of cards SEAT may legally play now."
|
|
|
|
|
(cl-remove-if-not (lambda (c) (cg-trick--legal-p game seat c))
|
|
|
|
|
(cg-trick--hand game seat)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--winner ((game cg-trick-game))
|
|
|
|
|
"Return the seat that wins the now-complete current trick."
|
|
|
|
|
(let* ((trick (reverse (cg-get game :trick))) ; play order
|
|
|
|
|
(led (car (cdr (car trick))))
|
|
|
|
|
(trump (oref game trump))
|
|
|
|
|
(best (car trick)))
|
|
|
|
|
(dolist (play (cdr trick))
|
|
|
|
|
(let ((bc (cdr best)) (pc (cdr play)))
|
|
|
|
|
(cond
|
|
|
|
|
((and trump (= (car pc) trump) (not (= (car bc) trump)))
|
|
|
|
|
(setq best play))
|
|
|
|
|
((and (= (car pc) (car bc)) (> (cdr pc) (cdr bc)))
|
|
|
|
|
(setq best play))
|
|
|
|
|
((and trump (not (= (car bc) trump)) (= (car pc) led)
|
|
|
|
|
(> (cdr pc) (cdr bc)))
|
|
|
|
|
(setq best play)))))
|
|
|
|
|
(car best)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--play ((game cg-trick-game) seat card)
|
|
|
|
|
"Have SEAT play CARD, resolving the trick when it completes."
|
|
|
|
|
(cg-trick--set-hand game seat (remove card (cg-trick--hand game seat)))
|
|
|
|
|
(when (= (car card) (oref game restricted)) (cg-put game :broken t))
|
|
|
|
|
(cg-put game :trick (cons (cons seat card) (cg-get game :trick)))
|
|
|
|
|
(if (= 4 (length (cg-get game :trick)))
|
|
|
|
|
(let* ((w (cg-trick--winner game))
|
|
|
|
|
(cards (mapcar #'cdr (cg-get game :trick))))
|
|
|
|
|
(aset (cg-get game :tricks) w (1+ (aref (cg-get game :tricks) w)))
|
|
|
|
|
(aset (cg-get game :taken) w (append cards (aref (cg-get game :taken) w)))
|
|
|
|
|
(cg-put game :trick nil)
|
|
|
|
|
(cg-put game :trick-no (1+ (cg-get game :trick-no)))
|
|
|
|
|
(cg-put game :leader w)
|
|
|
|
|
(cg-put game :turn w)
|
|
|
|
|
w)
|
|
|
|
|
(cg-put game :turn (mod (1+ seat) 4))
|
|
|
|
|
nil))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--hand-over-p (game)
|
|
|
|
|
"Return non-nil when all 13 tricks of the hand have been played."
|
|
|
|
|
(and (null (cg-get game :trick))
|
|
|
|
|
(cl-every #'null (append (cg-get game :hands) nil))))
|
|
|
|
|
|
|
|
|
|
;;;; Hearts specifics
|
|
|
|
|
|
|
|
|
|
(defun cg-hearts--card-points (card)
|
|
|
|
|
"Return the penalty points for CARD in Hearts."
|
|
|
|
|
(cond ((equal card '(0 . 10)) 13) ; Queen of Spades
|
|
|
|
|
((= (car card) 3) 1) ; any heart
|
|
|
|
|
(t 0)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--legal-p ((game cg-hearts-game) seat card)
|
|
|
|
|
"Hearts legality, adding the first-trick rules to the base."
|
|
|
|
|
(and (cl-call-next-method)
|
|
|
|
|
(let ((trick (cg-get game :trick))
|
|
|
|
|
(hand (cg-trick--hand game seat))
|
|
|
|
|
(first (= 0 (cg-get game :trick-no))))
|
|
|
|
|
(cond
|
|
|
|
|
;; the very first card of the hand must be the Two of Clubs
|
|
|
|
|
((and first (null trick))
|
|
|
|
|
(equal card '(1 . 0)))
|
|
|
|
|
;; no points on the first trick unless that is all one holds
|
|
|
|
|
((and first trick (> (cg-hearts--card-points card) 0))
|
|
|
|
|
(cl-every (lambda (c) (> (cg-hearts--card-points c) 0)) hand))
|
|
|
|
|
(t t)))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--leader-init ((game cg-hearts-game))
|
|
|
|
|
"Hearts: the holder of the Two of Clubs leads first."
|
|
|
|
|
(let (seat)
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(when (member '(1 . 0) (cg-trick--hand game s)) (setq seat s)))
|
|
|
|
|
(cg-put game :leader seat) (cg-put game :turn seat)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--leader-init ((game cg-spades-game))
|
|
|
|
|
"Spades: the player left of the dealer leads first."
|
|
|
|
|
(let ((s (mod (1+ (or (cg-get game :dealer) 3)) 4)))
|
|
|
|
|
(cg-put game :leader s) (cg-put game :turn s)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--score-hand ((game cg-hearts-game))
|
|
|
|
|
"Score a finished Hearts hand into the cumulative scores."
|
|
|
|
|
(let ((pts (make-vector 4 0)) (scores (cg-get game :scores)))
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(aset pts s (apply #'+ (mapcar #'cg-hearts--card-points
|
|
|
|
|
(aref (cg-get game :taken) s)))))
|
|
|
|
|
;; shooting the moon
|
|
|
|
|
(let ((moon (cl-position 26 (append pts nil))))
|
|
|
|
|
(if moon
|
|
|
|
|
(dotimes (s 4) (unless (= s moon)
|
|
|
|
|
(aset scores s (+ (aref scores s) 26))))
|
|
|
|
|
(dotimes (s 4) (aset scores s (+ (aref scores s) (aref pts s))))))
|
|
|
|
|
(cg-put game :last-points pts)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--game-over-p ((game cg-hearts-game))
|
|
|
|
|
"Hearts ends when any score reaches the target."
|
|
|
|
|
(cl-some (lambda (s) (>= s (oref game target))) (append (cg-get game :scores) nil)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--winner-seat ((game cg-hearts-game))
|
|
|
|
|
"Return the winning seat (lowest score) for a finished Hearts game."
|
|
|
|
|
(let ((best 0))
|
|
|
|
|
(dotimes (s 4) (when (< (aref (cg-get game :scores) s)
|
|
|
|
|
(aref (cg-get game :scores) best))
|
|
|
|
|
(setq best s)))
|
|
|
|
|
best))
|
|
|
|
|
|
|
|
|
|
;;;; Spades specifics
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--score-hand ((game cg-spades-game))
|
|
|
|
|
"Score a finished Spades hand into the cumulative team scores."
|
|
|
|
|
(let ((scores (cg-get game :scores))
|
|
|
|
|
(bags (cg-get game :bags))
|
|
|
|
|
(bids (cg-get game :bids))
|
|
|
|
|
(tricks (cg-get game :tricks)))
|
|
|
|
|
(dotimes (team 2)
|
|
|
|
|
(let* ((a team) (b (+ team 2))
|
|
|
|
|
(teambid 0) (teamtricks (+ (aref tricks a) (aref tricks b)))
|
|
|
|
|
(delta 0))
|
|
|
|
|
;; nil bids handled per player
|
|
|
|
|
(dolist (s (list a b))
|
|
|
|
|
(if (= (aref bids s) 0)
|
|
|
|
|
(setq delta (+ delta (if (= (aref tricks s) 0) 100 -100)))
|
|
|
|
|
(setq teambid (+ teambid (aref bids s)))))
|
|
|
|
|
(if (>= teamtricks teambid)
|
|
|
|
|
(let ((over (- teamtricks teambid)))
|
|
|
|
|
;; overtricks beyond nil winners count as bags
|
|
|
|
|
(setq delta (+ delta (* 10 teambid) over))
|
|
|
|
|
(aset bags team (+ (aref bags team) over))
|
|
|
|
|
(when (>= (aref bags team) 10)
|
|
|
|
|
(setq delta (- delta 100))
|
|
|
|
|
(aset bags team (- (aref bags team) 10))))
|
|
|
|
|
(setq delta (- delta (* 10 teambid))))
|
|
|
|
|
(aset scores a (+ (aref scores a) delta))
|
|
|
|
|
(aset scores b (aref scores a))))
|
|
|
|
|
(cg-put game :scores scores)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--game-over-p ((game cg-spades-game))
|
|
|
|
|
"Spades ends when a team reaches the target (or falls badly behind)."
|
|
|
|
|
(cl-some (lambda (s) (>= s (oref game target))) (append (cg-get game :scores) nil)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--winner-seat ((game cg-spades-game))
|
|
|
|
|
"Return a member seat of the winning team for a finished Spades game."
|
|
|
|
|
(if (>= (aref (cg-get game :scores) 0) (aref (cg-get game :scores) 1)) 0 1))
|
|
|
|
|
|
|
|
|
|
;;;; AI
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-bid ((game cg-spades-game) seat)
|
|
|
|
|
"Return a simple trick estimate (bid) for SEAT in Spades."
|
|
|
|
|
(let ((hand (cg-trick--hand game seat)) (bid 0))
|
|
|
|
|
(dolist (c hand)
|
|
|
|
|
(cond
|
|
|
|
|
((= (cdr c) 12) (setq bid (1+ bid))) ; aces
|
|
|
|
|
((and (= (cdr c) 11)) (setq bid (1+ bid))) ; kings
|
|
|
|
|
((and (= (car c) 0) (>= (cdr c) 9)) (setq bid (1+ bid))))) ; high spades
|
|
|
|
|
;; long spades add tricks
|
|
|
|
|
(let ((nsp (cl-count-if (lambda (c) (= (car c) 0)) hand)))
|
|
|
|
|
(when (> nsp 4) (setq bid (+ bid (- nsp 4)))))
|
|
|
|
|
(max 1 (min 13 bid))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-play ((game cg-hearts-game) seat)
|
|
|
|
|
"Choose a legal Hearts card for SEAT, avoiding points."
|
|
|
|
|
(let* ((moves (cg-trick--legal-moves game seat))
|
|
|
|
|
(trick (cg-get game :trick)))
|
|
|
|
|
(or
|
|
|
|
|
(if (null trick)
|
|
|
|
|
;; leading: play a low non-point card
|
|
|
|
|
(car (sort (copy-sequence moves)
|
|
|
|
|
(lambda (a b) (< (+ (* 4 (cg-hearts--card-points a)) (cdr a))
|
|
|
|
|
(+ (* 4 (cg-hearts--card-points b)) (cdr b))))))
|
|
|
|
|
;; following: if we can duck under the current winner, play highest
|
|
|
|
|
;; safe card; else dump the most dangerous card
|
|
|
|
|
(let* ((led (cg-trick--led-suit game))
|
|
|
|
|
(winrank (apply #'max (cons -1 (mapcar (lambda (p) (if (= (car (cdr p)) led)
|
|
|
|
|
(cdr (cdr p)) -1))
|
|
|
|
|
trick))))
|
|
|
|
|
(under (cl-remove-if-not (lambda (c) (and (= (car c) led)
|
|
|
|
|
(< (cdr c) winrank)))
|
|
|
|
|
moves)))
|
|
|
|
|
(cond
|
|
|
|
|
(under (car (last (cg-trick--sort under)))) ; highest still safe
|
|
|
|
|
((cl-some (lambda (c) (/= (car c) led)) moves) ; void: dump worst
|
|
|
|
|
(car (sort (copy-sequence moves)
|
|
|
|
|
(lambda (a b) (> (+ (* 4 (cg-hearts--card-points a)) (cdr a))
|
|
|
|
|
(+ (* 4 (cg-hearts--card-points b)) (cdr b)))))))
|
|
|
|
|
(t (car (sort (copy-sequence moves) ; must follow & take: lowest
|
|
|
|
|
(lambda (a b) (< (cdr a) (cdr b)))))))))
|
|
|
|
|
(car moves))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-play ((game cg-spades-game) seat)
|
|
|
|
|
"Choose a legal Spades card for SEAT."
|
|
|
|
|
(let* ((moves (cg-trick--legal-moves game seat))
|
|
|
|
|
(trick (cg-get game :trick))
|
|
|
|
|
(trump (oref game trump)))
|
|
|
|
|
(or
|
|
|
|
|
(if (null trick)
|
|
|
|
|
;; lead a high non-spade if possible, else lowest
|
|
|
|
|
(let ((non (cl-remove-if (lambda (c) (= (car c) trump)) moves)))
|
|
|
|
|
(if non (car (last (cg-trick--sort non)))
|
|
|
|
|
(car (cg-trick--sort moves))))
|
|
|
|
|
(let* ((led (cg-trick--led-suit game))
|
|
|
|
|
(cur (cg-get game :trick))
|
|
|
|
|
;; current winning play
|
|
|
|
|
(winner (cg-trick--winner-of game cur))
|
|
|
|
|
(partner-winning (and winner (= (cg-trick--team winner)
|
|
|
|
|
(cg-trick--team seat)))))
|
|
|
|
|
(if partner-winning
|
|
|
|
|
(car (cg-trick--sort moves)) ; let partner have it: play low
|
|
|
|
|
;; try to win cheaply
|
|
|
|
|
(let* ((followers (cl-remove-if-not (lambda (c) (= (car c) led)) moves)))
|
|
|
|
|
(or (car (cg-trick--sort followers))
|
|
|
|
|
(car (cg-trick--sort moves)))))))
|
|
|
|
|
(car moves))))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--winner-of (game trick)
|
|
|
|
|
"Return the seat currently winning the partial TRICK of GAME."
|
|
|
|
|
(when trick
|
|
|
|
|
(let* ((order (reverse trick))
|
|
|
|
|
(led (car (cdr (car order))))
|
|
|
|
|
(trump (oref game trump))
|
|
|
|
|
(best (car order)))
|
|
|
|
|
(dolist (play (cdr order))
|
|
|
|
|
(let ((bc (cdr best)) (pc (cdr play)))
|
|
|
|
|
(cond
|
|
|
|
|
((and trump (= (car pc) trump) (not (= (car bc) trump))) (setq best play))
|
|
|
|
|
((and (= (car pc) (car bc)) (> (cdr pc) (cdr bc))) (setq best play))
|
|
|
|
|
((and trump (not (= (car bc) trump)) (= (car pc) led)
|
|
|
|
|
(> (cdr pc) (cdr bc))) (setq best play)))))
|
|
|
|
|
(car best))))
|
|
|
|
|
|
|
|
|
|
;;;; Game driver (logic; UI layered on top)
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--start-hand ((game cg-trick-game))
|
|
|
|
|
"Deal and prepare a new hand, leaving GAME ready for the first lead."
|
|
|
|
|
(cg-trick--deal game)
|
|
|
|
|
(cg-trick--leader-init game)
|
|
|
|
|
game)
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--simulate-hand (game)
|
|
|
|
|
"Play a whole hand with AI for every seat (used by tests)."
|
|
|
|
|
(while (not (cg-trick--hand-over-p game))
|
|
|
|
|
(let ((seat (cg-get game :turn)))
|
|
|
|
|
(cg-trick--play game seat (cg-trick--ai-play game seat))))
|
|
|
|
|
(cg-trick--score-hand game))
|
|
|
|
|
|
|
|
|
|
;;;; New-game / hand lifecycle
|
|
|
|
|
|
|
|
|
|
(defvar-local cg-trick--game nil "The trick-taking game in the current buffer.")
|
|
|
|
|
|
|
|
|
|
(defconst cg-trick--pass-dirs [1 3 2 0]
|
|
|
|
|
"Pass directions by hand: left, right, across, hold (then repeat).")
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--dir-name (dir)
|
|
|
|
|
"Return a human label for pass direction DIR."
|
|
|
|
|
(pcase dir (1 "left") (3 "right") (2 "across") (_ "hold")))
|
|
|
|
|
|
|
|
|
|
(cl-defgeneric cg-trick--begin-hand (game)
|
|
|
|
|
"Deal and set up a new hand of GAME, then run AI up to the human's turn.")
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--begin-hand ((game cg-hearts-game))
|
|
|
|
|
(cg-trick--deal game)
|
|
|
|
|
(cg-put game :hand-no (1+ (or (cg-get game :hand-no) 0)))
|
|
|
|
|
(cg-put game :cursor 0) (cg-put game :marks nil)
|
|
|
|
|
(let ((dir (aref cg-trick--pass-dirs (mod (1- (cg-get game :hand-no)) 4))))
|
|
|
|
|
(cg-put game :pass-dir dir)
|
|
|
|
|
(if (= dir 0)
|
|
|
|
|
(progn (cg-trick--leader-init game)
|
|
|
|
|
(cg-put game :phase 'play)
|
|
|
|
|
(cg-put game :message "No passing this hand. Play begins.")
|
|
|
|
|
(cg-trick--run game))
|
|
|
|
|
(cg-put game :phase 'pass)
|
|
|
|
|
(cg-put game :message
|
|
|
|
|
(format "Pass three cards %s. RET marks a card; p sends them."
|
|
|
|
|
(cg-trick--dir-name dir))))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--begin-hand ((game cg-spades-game))
|
|
|
|
|
(cg-trick--deal game)
|
|
|
|
|
(cg-put game :dealer (mod (1+ (or (cg-get game :dealer) 3)) 4))
|
|
|
|
|
(cg-put game :cursor 0)
|
|
|
|
|
(let ((bids (make-vector 4 0)))
|
|
|
|
|
(dotimes (s 4) (unless (= s 0) (aset bids s (cg-trick--ai-bid game s))))
|
|
|
|
|
(aset bids 0 (if noninteractive (cg-trick--ai-bid game 0)
|
|
|
|
|
(let ((sug (cg-trick--ai-bid game 0)))
|
|
|
|
|
(max 0 (min 13 (read-number
|
|
|
|
|
(format "Your bid (0 = nil) [suggest %d]: " sug)
|
|
|
|
|
sug))))))
|
|
|
|
|
(cg-put game :bids bids))
|
|
|
|
|
(cg-trick--leader-init game)
|
|
|
|
|
(cg-put game :phase 'play)
|
|
|
|
|
(cg-put game :message
|
|
|
|
|
(format "You bid %d. Make your side's combined bid."
|
|
|
|
|
(aref (cg-get game :bids) 0)))
|
|
|
|
|
(cg-trick--run game))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--new (game)
|
|
|
|
|
"Initialise GAME for a fresh match and deal the first hand."
|
|
|
|
|
(cg-put game :scores (make-vector 4 0))
|
|
|
|
|
(cg-put game :bags (make-vector 2 0))
|
|
|
|
|
(cg-put game :dealer 3)
|
|
|
|
|
(cg-put game :hand-no 0)
|
|
|
|
|
(cg-put game :round 0)
|
|
|
|
|
(cg-trick--begin-hand game)
|
|
|
|
|
game)
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--run (game)
|
|
|
|
|
"Advance AI seats until it is the human's turn or the hand ends."
|
|
|
|
|
(while (and (eq (cg-get game :phase) 'play)
|
|
|
|
|
(not (cg-trick--hand-over-p game))
|
|
|
|
|
(/= (cg-get game :turn) 0))
|
|
|
|
|
(let ((s (cg-get game :turn)))
|
|
|
|
|
(cg-trick--play game s (cg-trick--ai-play game s))))
|
|
|
|
|
(when (and (eq (cg-get game :phase) 'play) (cg-trick--hand-over-p game))
|
|
|
|
|
(cg-trick--finish-hand game)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--finish-hand (game)
|
|
|
|
|
"Score the finished hand of GAME and start the next, or end the match."
|
|
|
|
|
(cg-trick--score-hand game)
|
|
|
|
|
(if (cg-trick--game-over-p game)
|
|
|
|
|
(progn (cg-put game :phase 'game-over)
|
|
|
|
|
(cg-put game :message
|
|
|
|
|
(format "Game over. %s. Press n for a new match."
|
|
|
|
|
(cg-trick--result-string game))))
|
|
|
|
|
(cg-trick--begin-hand game)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--result-string ((game cg-hearts-game))
|
|
|
|
|
(format "%s wins with the lowest score"
|
|
|
|
|
(aref cg-trick-seat-names (cg-trick--winner-seat game))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--result-string ((game cg-spades-game))
|
|
|
|
|
(let ((w (cg-trick--winner-seat game)))
|
|
|
|
|
(format "%s win" (if (= w 0) "You and North" "West and East"))))
|
|
|
|
|
|
|
|
|
|
;;;; AI passing
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-pass ((_ cg-hearts-game) hand)
|
|
|
|
|
"Return three cards to pass from HAND (shed the most dangerous)."
|
|
|
|
|
(let ((danger (lambda (c) (+ (* 6 (cg-hearts--card-points c))
|
|
|
|
|
(if (and (= (car c) 0) (>= (cdr c) 10)) 5 0)
|
|
|
|
|
(cdr c)))))
|
|
|
|
|
(cl-subseq (sort (copy-sequence hand)
|
|
|
|
|
(lambda (a b) (> (funcall danger a) (funcall danger b))))
|
|
|
|
|
0 3)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--do-pass (game)
|
|
|
|
|
"Exchange the chosen passing cards among the four seats of GAME."
|
|
|
|
|
(let* ((dir (cg-get game :pass-dir))
|
|
|
|
|
(sel (make-vector 4 nil))
|
|
|
|
|
(kept (make-vector 4 nil)))
|
|
|
|
|
(aset sel 0 (copy-sequence (cg-get game :marks)))
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(unless (= s 0)
|
|
|
|
|
(aset sel s (copy-sequence (cg-trick--ai-pass game (cg-trick--hand game s))))))
|
|
|
|
|
;; what each seat keeps (its hand minus the cards it gives away)
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(aset kept s (cl-remove-if (lambda (c) (member c (aref sel s)))
|
|
|
|
|
(cg-trick--hand game s))))
|
|
|
|
|
;; deal each seat's three cards to the seat DIR places along
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(let ((r (mod (+ s dir) 4)))
|
|
|
|
|
(aset kept r (append (aref kept r) (aref sel s)))))
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(cg-trick--set-hand game s (cg-trick--sort (aref kept s))))
|
|
|
|
|
(cg-trick--leader-init game)
|
|
|
|
|
(cg-put game :phase 'play)
|
|
|
|
|
(cg-put game :marks nil)
|
|
|
|
|
(cg-put game :message "Cards passed. Play begins.")
|
|
|
|
|
(cg-trick--run game)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--seat-line (game s)
|
|
|
|
|
"Return a status line for opponent seat S of GAME."
|
|
|
|
|
(let* ((n (length (cg-trick--hand game s)))
|
|
|
|
|
(bid (and (cg-get game :bids) (aref (cg-get game :bids) s)))
|
|
|
|
|
(won (and (cg-get game :tricks) (aref (cg-get game :tricks) s))))
|
|
|
|
|
(format " %-6s %2d cards%s%s\n" (aref cg-trick-seat-names s) n
|
|
|
|
|
(if bid (format " bid %d" bid) "")
|
|
|
|
|
(if won (format " won %d" won) ""))))
|
|
|
|
|
|
2026-06-25 07:20:03 -05:00
|
|
|
(defcustom cg-trick-svg-cards t
|
|
|
|
|
"When non-nil, draw cards as SVG images on a graphical display."
|
|
|
|
|
:type 'boolean :group 'card-games)
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--spec (card)
|
|
|
|
|
"Return the cg-svg display spec (RANK-STRING . SUIT) for CARD."
|
|
|
|
|
(cons (aref cg-trick-ranks (cdr card)) (car card)))
|
|
|
|
|
|
2026-06-26 15:30:46 -05:00
|
|
|
(cl-defun cg-trick--svg-row (cards &key cursor marks hints region-tag)
|
|
|
|
|
"Return a one-image SVG row for CARDS (clickable + sliderful when REGION-TAG)."
|
|
|
|
|
(cg-svg-hand-image (mapcar #'cg-trick--spec cards)
|
|
|
|
|
:cursor cursor :marks marks :hints hints
|
|
|
|
|
:overlap (if (> (length cards) 11)
|
|
|
|
|
(max 0 (- cg-svg-card-width 24)) 0)
|
|
|
|
|
:region-tag region-tag))
|
2026-06-25 07:20:03 -05:00
|
|
|
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(cl-defmethod cg-render ((game cg-trick-game))
|
|
|
|
|
"Return a propertized string depicting GAME for a text display."
|
|
|
|
|
(let* ((out (list))
|
|
|
|
|
(scores (cg-get game :scores))
|
|
|
|
|
(marks (cg-get game :marks))
|
|
|
|
|
(cursor (cg-get game :cursor))
|
|
|
|
|
(hand (cg-trick--sort (cg-trick--hand game 0))))
|
|
|
|
|
(push (format " %s\n" (oref game vname)) out)
|
|
|
|
|
(when scores
|
|
|
|
|
(push (format " Scores: South %d West %d North %d East %d\n\n"
|
|
|
|
|
(aref scores 0) (aref scores 1) (aref scores 2) (aref scores 3))
|
|
|
|
|
out))
|
|
|
|
|
(dolist (s '(2 1 3)) ; North, West, East
|
|
|
|
|
(push (cg-trick--seat-line game s) out))
|
|
|
|
|
;; current trick
|
|
|
|
|
(push "\n Trick: " out)
|
2026-06-25 07:20:03 -05:00
|
|
|
(cond
|
|
|
|
|
((null (cg-get game :trick)) (push "(empty)" out))
|
|
|
|
|
((and cg-trick-svg-cards (display-graphic-p))
|
|
|
|
|
(push (concat (mapconcat (lambda (p) (aref cg-trick-seat-names (car p)))
|
|
|
|
|
(reverse (cg-get game :trick)) " ") " ")
|
|
|
|
|
out)
|
|
|
|
|
(push (cg-trick--svg-row (mapcar #'cdr (reverse (cg-get game :trick)))) out))
|
|
|
|
|
(t (dolist (play (reverse (cg-get game :trick)))
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(push (format "%s:%s " (aref cg-trick-seat-names (car play))
|
|
|
|
|
(let ((cs (cg-trick-card-string (cdr play))))
|
|
|
|
|
(if (cg-trick-red-p (cdr play))
|
|
|
|
|
(propertize cs 'face 'cg-red-suit) cs)))
|
2026-06-25 07:20:03 -05:00
|
|
|
out))))
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(push "\n\n Your hand (South):\n " out)
|
2026-06-25 07:20:03 -05:00
|
|
|
(if (and cg-trick-svg-cards (display-graphic-p))
|
|
|
|
|
(let ((mi '()) (hi '()) (i 0)
|
|
|
|
|
(legalp (and (eq (cg-get game :phase) 'play) (= (cg-get game :turn) 0))))
|
|
|
|
|
(dolist (c hand)
|
|
|
|
|
(when (member c marks) (push i mi))
|
|
|
|
|
(when (and legalp (cg-trick--legal-p game 0 c)) (push i hi))
|
|
|
|
|
(setq i (1+ i)))
|
2026-06-26 15:30:46 -05:00
|
|
|
(push (cg-trick--svg-row hand :cursor cursor :marks mi :hints hi
|
|
|
|
|
:region-tag 'hand) out))
|
2026-06-25 07:20:03 -05:00
|
|
|
(let ((i 0))
|
|
|
|
|
(dolist (c hand)
|
|
|
|
|
(let* ((cs (cg-trick-card-string c))
|
|
|
|
|
(faces nil))
|
|
|
|
|
(when (cg-trick-red-p c) (push 'cg-red-suit faces))
|
|
|
|
|
(when (member c marks) (push 'cg-hint faces))
|
|
|
|
|
(when (= i cursor) (push 'cg-cursor faces))
|
|
|
|
|
(push (propertize (format "%4s" cs) 'face (or faces 'default)) out))
|
|
|
|
|
(setq i (1+ i)))))
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(push (format "\n\n %s\n" (cg-get game :message)) out)
|
|
|
|
|
(apply #'concat (nreverse out))))
|
|
|
|
|
|
2026-06-26 15:30:46 -05:00
|
|
|
(cl-defmethod cg-render-apply ((g cg-trick-game) action)
|
|
|
|
|
"Apply a click ACTION on the hand: select that card and play it."
|
|
|
|
|
(pcase action
|
|
|
|
|
(`(hand . ,i) (cg-put g :cursor i) (cg-trick-act))
|
|
|
|
|
(_ (cl-call-next-method))))
|
|
|
|
|
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(defun cg-trick--redisplay ()
|
|
|
|
|
"Redraw the current trick-game buffer."
|
|
|
|
|
(let ((game cg-trick--game) (inhibit-read-only t))
|
2026-06-26 15:30:46 -05:00
|
|
|
(setq cg-current-game game cg-redisplay-function #'cg-trick--redisplay)
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(setq-local mode-line-process
|
|
|
|
|
(format " [%s]" (or (cg-get game :phase) "play")))
|
|
|
|
|
(erase-buffer)
|
|
|
|
|
(insert (cg-render game))
|
|
|
|
|
(goto-char (point-min))))
|
|
|
|
|
|
|
|
|
|
;;;; Commands
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--cursor-card (game)
|
|
|
|
|
"Return the South card currently under the cursor."
|
|
|
|
|
(nth (cg-get game :cursor) (cg-trick--sort (cg-trick--hand game 0))))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-left ()
|
|
|
|
|
"Move the hand cursor left."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((game cg-trick--game) (n (length (cg-trick--hand game 0))))
|
|
|
|
|
(when (> n 0) (cg-put game :cursor (mod (1- (cg-get game :cursor)) n)))
|
|
|
|
|
(cg-trick--redisplay)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-right ()
|
|
|
|
|
"Move the hand cursor right."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((game cg-trick--game) (n (length (cg-trick--hand game 0))))
|
|
|
|
|
(when (> n 0) (cg-put game :cursor (mod (1+ (cg-get game :cursor)) n)))
|
|
|
|
|
(cg-trick--redisplay)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-act ()
|
|
|
|
|
"Play, or (during the Hearts pass) mark, the selected card."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let* ((game cg-trick--game)
|
|
|
|
|
(phase (cg-get game :phase))
|
|
|
|
|
(card (cg-trick--cursor-card game)))
|
|
|
|
|
(pcase phase
|
|
|
|
|
('play
|
|
|
|
|
(cond
|
|
|
|
|
((/= (cg-get game :turn) 0) (cg-put game :message "Not your turn."))
|
|
|
|
|
((not (cg-trick--legal-p game 0 card))
|
|
|
|
|
(cg-put game :message "Illegal play — you must follow suit."))
|
|
|
|
|
(t (cg-trick--play game 0 card)
|
|
|
|
|
(cg-put game :cursor (max 0 (min (cg-get game :cursor)
|
|
|
|
|
(1- (length (cg-trick--hand game 0))))))
|
|
|
|
|
(cg-trick--run game))))
|
|
|
|
|
('pass
|
|
|
|
|
(if (member card (cg-get game :marks))
|
|
|
|
|
(cg-put game :marks (remove card (cg-get game :marks)))
|
|
|
|
|
(if (>= (length (cg-get game :marks)) 3)
|
|
|
|
|
(cg-put game :message "Three already marked — press p to pass.")
|
|
|
|
|
(cg-put game :marks (cons card (cg-get game :marks))))))
|
|
|
|
|
(_ (cg-put game :message "Press n for a new match.")))
|
|
|
|
|
(cg-trick--redisplay)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-pass ()
|
|
|
|
|
"Confirm the Hearts pass once three cards are marked."
|
|
|
|
|
(interactive)
|
|
|
|
|
(let ((game cg-trick--game))
|
|
|
|
|
(if (and (eq (cg-get game :phase) 'pass) (= 3 (length (cg-get game :marks))))
|
|
|
|
|
(cg-trick--do-pass game)
|
|
|
|
|
(cg-put game :message "Mark exactly three cards first."))
|
|
|
|
|
(cg-trick--redisplay)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-new ()
|
|
|
|
|
"Start a fresh match in this buffer."
|
|
|
|
|
(interactive)
|
|
|
|
|
(cg-trick--new cg-trick--game)
|
|
|
|
|
(cg-trick--redisplay))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-redraw () "Redraw the table." (interactive) (cg-trick--redisplay))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick-help ()
|
|
|
|
|
"Describe the controls."
|
|
|
|
|
(interactive)
|
|
|
|
|
(message "Arrows: choose card RET: play/mark p: pass (Hearts) n: new g: redraw"))
|
|
|
|
|
|
|
|
|
|
(defvar cg-trick-mode-map
|
|
|
|
|
(let ((map (make-sparse-keymap)))
|
2026-06-26 15:30:46 -05:00
|
|
|
(define-key map [mouse-1] #'cg-card-click)
|
|
|
|
|
(define-key map "+" #'cg-card-zoom-in)
|
|
|
|
|
(define-key map "=" #'cg-card-zoom-in)
|
|
|
|
|
(define-key map "-" #'cg-card-zoom-out)
|
|
|
|
|
(define-key map "0" #'cg-card-zoom-reset)
|
Add 16 games and known-games research; bump to 1.0.60
Console UNICODE games on the cg-core EIEIO engine, all ERT-tested (88/88):
- Tableau solitaire (cg-solitaire.el): Klondike, FreeCell, Spider, Yukon,
Canfield, Forty Thieves, Scorpion.
- Pile solitaire (cg-patience.el): Golf, TriPeaks, Pyramid.
- Trick-taking (cg-trick.el): Hearts, Spades, Whist, Oh Hell.
- Shedding/climbing: Crazy Eights (cg-eights.el), President (cg-president.el).
Wire all into the card-game chooser, Makefile, and README; add
known-games.org research collation; bump every file to 1.0.60.
2026-06-25 01:58:24 -05:00
|
|
|
(define-key map (kbd "<left>") #'cg-trick-left)
|
|
|
|
|
(define-key map (kbd "<right>") #'cg-trick-right)
|
|
|
|
|
(define-key map (kbd "RET") #'cg-trick-act)
|
|
|
|
|
(define-key map (kbd "SPC") #'cg-trick-act)
|
|
|
|
|
(define-key map "p" #'cg-trick-pass)
|
|
|
|
|
(define-key map "n" #'cg-trick-new)
|
|
|
|
|
(define-key map "g" #'cg-trick-redraw)
|
|
|
|
|
(define-key map "?" #'cg-trick-help)
|
|
|
|
|
map)
|
|
|
|
|
"Keymap for `cg-trick-mode'.")
|
|
|
|
|
|
|
|
|
|
(define-derived-mode cg-trick-mode special-mode "Trick"
|
|
|
|
|
"Major mode for the four-handed trick-taking games."
|
|
|
|
|
(setq-local truncate-lines t))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--play-game (class)
|
|
|
|
|
"Start a trick game of CLASS in its own buffer."
|
|
|
|
|
(let* ((game (make-instance class))
|
|
|
|
|
(buf (get-buffer-create (format "*%s*" (oref game vname)))))
|
|
|
|
|
(with-current-buffer buf
|
|
|
|
|
(cg-trick-mode)
|
|
|
|
|
(setq cg-trick--game game)
|
|
|
|
|
(cg-trick--new game)
|
|
|
|
|
(cg-trick--redisplay))
|
|
|
|
|
(switch-to-buffer buf)))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun cg-hearts ()
|
|
|
|
|
"Play Hearts against three computer opponents."
|
|
|
|
|
(interactive) (cg-trick--play-game 'cg-hearts-game))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun cg-spades ()
|
|
|
|
|
"Play Spades (partnership) against three computer opponents."
|
|
|
|
|
(interactive) (cg-trick--play-game 'cg-spades-game))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;;; Whist and Oh Hell
|
|
|
|
|
|
|
|
|
|
(defclass cg-whist-game (cg-trick-game)
|
|
|
|
|
((restricted :initform -1) (target :initform 5) (vname :initform "Whist"))
|
|
|
|
|
"Whist: trump set by the turned card, no bidding, score tricks over six.")
|
|
|
|
|
|
|
|
|
|
(defclass cg-ohhell-game (cg-trick-game)
|
|
|
|
|
((restricted :initform -1) (target :initform 0) (vname :initform "Oh Hell"))
|
|
|
|
|
"Oh Hell: hand size shrinks each round; bid the exact tricks you will take.")
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--leader-init ((game cg-whist-game))
|
|
|
|
|
(let ((s (mod (1+ (or (cg-get game :dealer) 3)) 4)))
|
|
|
|
|
(cg-put game :leader s) (cg-put game :turn s)))
|
|
|
|
|
(cl-defmethod cg-trick--leader-init ((game cg-ohhell-game))
|
|
|
|
|
(let ((s (mod (1+ (or (cg-get game :dealer) 3)) 4)))
|
|
|
|
|
(cg-put game :leader s) (cg-put game :turn s)))
|
|
|
|
|
|
|
|
|
|
(defun cg-trick--ai-trump-play (game seat)
|
|
|
|
|
"A generic legal trump-game play for SEAT: follow and win cheaply, else low."
|
|
|
|
|
(let* ((moves (cg-trick--legal-moves game seat))
|
|
|
|
|
(trick (cg-get game :trick)))
|
|
|
|
|
(or
|
|
|
|
|
(if (null trick)
|
|
|
|
|
(car (last (cg-trick--sort moves)))
|
|
|
|
|
(let* ((winner (cg-trick--winner-of game trick))
|
|
|
|
|
(partner-winning (and winner (= (cg-trick--team winner)
|
|
|
|
|
(cg-trick--team seat))))
|
|
|
|
|
(led (cg-trick--led-suit game)))
|
|
|
|
|
(if partner-winning
|
|
|
|
|
(car (cg-trick--sort moves))
|
|
|
|
|
(let ((follow (cl-remove-if-not (lambda (c) (= (car c) led)) moves)))
|
|
|
|
|
(or (car (last (cg-trick--sort follow)))
|
|
|
|
|
(car (cg-trick--sort moves)))))))
|
|
|
|
|
(car moves))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-play ((game cg-whist-game) seat)
|
|
|
|
|
(cg-trick--ai-trump-play game seat))
|
|
|
|
|
(cl-defmethod cg-trick--ai-play ((game cg-ohhell-game) seat)
|
|
|
|
|
(cg-trick--ai-trump-play game seat))
|
|
|
|
|
|
|
|
|
|
;; Whist
|
|
|
|
|
(cl-defmethod cg-trick--begin-hand ((game cg-whist-game))
|
|
|
|
|
(cg-trick--deal game)
|
|
|
|
|
(cg-put game :dealer (mod (1+ (or (cg-get game :dealer) 3)) 4))
|
|
|
|
|
(oset game trump (car (cg-get game :last-card))) ; dealer's last card turns trump
|
|
|
|
|
(cg-put game :cursor 0)
|
|
|
|
|
(cg-trick--leader-init game)
|
|
|
|
|
(cg-put game :phase 'play)
|
|
|
|
|
(cg-put game :message (format "Trump is %s. Take tricks past the book of six."
|
|
|
|
|
(cg-suit-glyph (oref game trump))))
|
|
|
|
|
(cg-trick--run game))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--score-hand ((game cg-whist-game))
|
|
|
|
|
(let ((scores (cg-get game :scores)) (tricks (cg-get game :tricks)))
|
|
|
|
|
(dotimes (team 2)
|
|
|
|
|
(let ((over (max 0 (- (+ (aref tricks team) (aref tricks (+ team 2))) 6))))
|
|
|
|
|
(aset scores team (+ (aref scores team) over))
|
|
|
|
|
(aset scores (+ team 2) (aref scores team))))
|
|
|
|
|
(cg-put game :scores scores)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--game-over-p ((game cg-whist-game))
|
|
|
|
|
(cl-some (lambda (s) (>= s (oref game target))) (append (cg-get game :scores) nil)))
|
|
|
|
|
(cl-defmethod cg-trick--winner-seat ((game cg-whist-game))
|
|
|
|
|
(if (>= (aref (cg-get game :scores) 0) (aref (cg-get game :scores) 1)) 0 1))
|
|
|
|
|
(cl-defmethod cg-trick--result-string ((game cg-whist-game))
|
|
|
|
|
(format "%s win" (if (= 0 (cg-trick--winner-seat game)) "You and North" "West and East")))
|
|
|
|
|
|
|
|
|
|
;; Oh Hell
|
|
|
|
|
(defconst cg-ohhell--sizes [7 6 5 4 3 2 1]
|
|
|
|
|
"Hand sizes dealt in successive Oh Hell rounds.")
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--ai-bid ((game cg-ohhell-game) seat)
|
|
|
|
|
(let ((hand (cg-trick--hand game seat)) (trump (oref game trump)) (bid 0))
|
|
|
|
|
(dolist (c hand)
|
|
|
|
|
(cond ((= (cdr c) 12) (cl-incf bid))
|
|
|
|
|
((and (= (car c) trump) (>= (cdr c) 9)) (cl-incf bid))))
|
|
|
|
|
(min bid (length hand))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--begin-hand ((game cg-ohhell-game))
|
|
|
|
|
(let* ((round (or (cg-get game :round) 0))
|
|
|
|
|
(hs (aref cg-ohhell--sizes (min round (1- (length cg-ohhell--sizes))))))
|
|
|
|
|
(oset game hand-size hs)
|
|
|
|
|
(cg-trick--deal game)
|
|
|
|
|
(cg-put game :dealer (mod (1+ (or (cg-get game :dealer) 3)) 4))
|
|
|
|
|
(let ((up (car (cg-get game :deck))))
|
|
|
|
|
(oset game trump (if up (car up) 0)))
|
|
|
|
|
(cg-put game :cursor 0)
|
|
|
|
|
(let ((bids (make-vector 4 0)))
|
|
|
|
|
(dotimes (s 4) (unless (= s 0) (aset bids s (cg-trick--ai-bid game s))))
|
|
|
|
|
(aset bids 0 (if noninteractive (cg-trick--ai-bid game 0)
|
|
|
|
|
(max 0 (min hs (read-number
|
|
|
|
|
(format "Round %d (trump %s) -- your bid (0-%d): "
|
|
|
|
|
(1+ round) (cg-suit-glyph (oref game trump)) hs)
|
|
|
|
|
(cg-trick--ai-bid game 0))))))
|
|
|
|
|
(cg-put game :bids bids))
|
|
|
|
|
(cg-trick--leader-init game)
|
|
|
|
|
(cg-put game :phase 'play)
|
|
|
|
|
(cg-put game :message (format "Round %d: make EXACTLY your bid (trump %s)."
|
|
|
|
|
(1+ round) (cg-suit-glyph (oref game trump))))
|
|
|
|
|
(cg-trick--run game)))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--score-hand ((game cg-ohhell-game))
|
|
|
|
|
(let ((scores (cg-get game :scores)) (bids (cg-get game :bids))
|
|
|
|
|
(tricks (cg-get game :tricks)))
|
|
|
|
|
(dotimes (s 4)
|
|
|
|
|
(when (= (aref tricks s) (aref bids s))
|
|
|
|
|
(aset scores s (+ (aref scores s) 10 (aref bids s)))))
|
|
|
|
|
(cg-put game :scores scores)
|
|
|
|
|
(cg-put game :round (1+ (or (cg-get game :round) 0)))))
|
|
|
|
|
|
|
|
|
|
(cl-defmethod cg-trick--game-over-p ((game cg-ohhell-game))
|
|
|
|
|
(>= (or (cg-get game :round) 0) (length cg-ohhell--sizes)))
|
|
|
|
|
(cl-defmethod cg-trick--winner-seat ((game cg-ohhell-game))
|
|
|
|
|
(let ((best 0)) (dotimes (s 4) (when (> (aref (cg-get game :scores) s)
|
|
|
|
|
(aref (cg-get game :scores) best)) (setq best s)))
|
|
|
|
|
best))
|
|
|
|
|
(cl-defmethod cg-trick--result-string ((game cg-ohhell-game))
|
|
|
|
|
(format "%s wins" (aref cg-trick-seat-names (cg-trick--winner-seat game))))
|
|
|
|
|
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun cg-whist () "Play Whist against three computer opponents."
|
|
|
|
|
(interactive) (cg-trick--play-game 'cg-whist-game))
|
|
|
|
|
;;;###autoload
|
|
|
|
|
(defun cg-ohhell () "Play Oh Hell against three computer opponents."
|
|
|
|
|
(interactive) (cg-trick--play-game 'cg-ohhell-game))
|
|
|
|
|
|
|
|
|
|
(provide 'cg-trick)
|
|
|
|
|
;;; cg-trick.el ends here
|