Designing a deck of cards in Bash with a functional programming approach

I wanted to solve a typical object-oriented problem in Bash, but using a functional programming approach instead: "What would a textbook OOP problem be?" First thing that came to mind was "Design a deck of cards". And although I don't have much of an interest in card games, that was just what I decided to code.

Now, I didn't want to go overboard. A proof-of-concept with potential for being extended seemed more than enough for the purpose, which was... what was the purpose again?

I'd say it was:

  1. to test the suitability of non-OOP approaches to a typical OOP problem, and
  2. to write concise, beautiful code — and with that redeem some of Bash's not-completely-fair reputation of being made of unreadable ugliness.

Before showing you the code, let me show you the results. Here's a demo — the output of an example proto-game that I "played" after I ran ./playcards.sh and kept selecting options from the menu:

Welcome to cards
----------------
1) New deck    3) Add people  5) Show cards
2) Shuffle     4) Deal cards  6) Quit
#? 1
New deck is ready. Shuffle it.

#? 2
Shuffled.

#? 3
Names, separated by space: Alice Bob Charlie
People:  Alice Bob Charlie

#? 3
Names, separated by space: Denzel Emily
People:  Alice Bob Charlie Denzel Emily

#? 4
How many cards? 10
To whom?
1) Everybody  3) Bob        5) Denzel
2) Alice      4) Charlie    6) Emily

#? 1
 5♠  2♠  2♣  6♠  7♣  9♣  5♦  8♠  J♣  7♦  ⇒  Alice
 3♣ 10♣  7♥  A♣  3♦  4♥  Q♥  K♦  6♣  Q♠  ⇒  Bob
 Q♦  K♠  A♦  2♦  9♠  A♠  8♦  3♥ 10♦  4♦  ⇒  Charlie
 8♥  9♦  J♥  K♣  3♠  8♣  Q♣  K♥  4♠ 10♠  ⇒  Denzel
10♥  J♠  9♥  A♥  2♥  J♦  6♥  7♠  5♣  6♦  ⇒  Emily

#? 4
How many cards? 2
To whom?
1) Everybody  3) Bob        5) Denzel
2) Alice      4) Charlie    6) Emily

#? 2
 4♣  5♥  ⇒  Alice

#?
1) New deck    3) Add people  5) Show cards
2) Shuffle     4) Deal cards  6) Quit
#? 5
Whose cards?
1) Everybody  3) Bob        5) Denzel
2) Alice      4) Charlie    6) Emily

#? 1
Alice          Bob           Charlie           Denzel      Emily
♣: 2 4 7 9 J   ♣: A 3 6 10   ♣:                ♣: 8 Q K    ♣: 5
♦: 5 7         ♦: 3 K        ♦: A 2 4 8 10 Q   ♦: 9        ♦: 6 J
♥: 5           ♥: 4 7 Q      ♥: 3              ♥: 8 J K    ♥: A 2 6 9 10
♠: 2 5 6 8     ♠: Q          ♠: A 9 K          ♠: 3 4 10   ♠: 7 J

#? 6
Goodbye!

Notice that the whole deck (5×10 + 2 = 13×4) ended up being dealt to the five people.

So could Bash be used to extend this to some arbitrary game of cards?

I suppose so. This took ~100 lines of code, and much of the basics are here: create a deck, shuffle cards, create a list of players, distribute cards, and keep track of which cards are held by which players.

What else would an actual game of card demand?

It would depend on the game, of course. (Bridge, for example, should prove harder.) But probably: dealing with more than one deck, adding Jokers, assigning game-based values to cards, comparing cards based on these values, calculating the value of a player's hand, keeping track of sequences laid down on the table, and a few other operations for which functions would need to be written.

There are all sorts of reasons to pick something other than Bash to implement your card game application in. Graphics, for one, unless your players love the terminal. Speed, also, since Bash's is particularly terrible. Portability, perhaps. Etc. It seems to me, however, that "cards are objects, therefore I must use an object-oriented language" would not be a great reason.

Here is the code:

#!/usr/bin/env bash
#
# SPDX-FileCopyrightText:  © flandrew
# SPDX-License-Identifier: GPL-3.0-or-later
#
# playcards.sh — Designing a deck of cards — Version: 0.1.1
#
#   Dependencies:
#   - Package 'moreutils' <http://joeyh.name/code/moreutils/> is needed
#     because of 'combine'. The rest is standard GNU coreutils and gsed.
#
#   Notes:
#   - Validation of user input has not been extensively tested.
#   - Diffs and Adds assume deck has no repeated cards. With more than one
#     deck, this would need tweaking.

### Functions

# Auxiliary — No side-effects
# ----------------------------------------------------------------------------
byline()    { tr ' ' '\n';}
sanitizeA() { tr -cd a-zA-Z    <<< "$1";}
sanitizeS() { tr -cd a-zA-Z' ' <<< "$1";}
sanitizeN() { tr -cd 0-9       <<< "$1";}
threechar() { sed -E "s/\b[A234567890JQK]/ &/g";}
column-it() { sed "s/ /\xc2\xa0/g"   | pr -t"$1"Tw "$(tput cols)" -l 300 |
              sed -E "s/[ \t]+/\t/g" | column -Lts$'\t' |
              sed "s/\xc2\xa0/ /g";}


# Core — No side-effects
# ----------------------------------------------------------------------------
whatis()    { local x; x="$(sanitizeA "$1")"
              . <(echo echo \""\${$x[@]}"\");}
arraydiff() { combine <(whatis "$1" | byline) not \
                      <(whatis "$2" | byline) | xargs;}
arrayadd()  { combine <(whatis "$1" | byline) or  \
                      <(whatis "$2" | byline) | xargs;}
arrayadduq(){ combine <(whatis "$1" | byline) or  \
                      <(whatis "$2" | byline) | awk '!x[$0]++' | xargs;}
shuffle()   { whatis "$1" | byline | shuf | xargs;}
sortreg()   { local w; w="$(whatis "$1")"
              for s in "${suits[@]}"; do
                  for v in "${values[@]}"; do
                      grep -oE "$v$s" <<< "$w"
                  done
              done | xargs;}
sortgrp()   { local r; r="$(sortreg "$1")"
              for s in "${suits[@]}"; do
                  grep -oE "[^ ]+$s" <<< "$r" |
                      sed "s/$s$//" | xargs   |
                      sed -E "s/.*/$s: & /"
              done;}
deal()      { whatis "$1" | sed "s/ /\n/${2:-1}" | head -n1;}


# Core — Side-effects
# ----------------------------------------------------------------------------
specdeck()  { values=(A 2 3 4 5 6 7 8 9 10 J Q K)
              suits=(♣ ♦ ♥ ♠);}
makedeck()  { specdeck
              deck=($(for s in "${suits[@]}"; do
                          for v in "${values[@]}"; do
                              printf "%s " "$v$s"
                          done
                      done));}
nshuffle()  { readarray -t "$1" < <(shuffle "$1");}
ndeal()     { local x y z  # deckname playername numberofcards
              x="$(sanitizeA "$1")"     ; [[ -z "$x" ]] && x=deck
              y="$(sanitizeA "$2")"     ; [[ -z "$y" ]] && y=Nameless
              z="$(sanitizeN "${3:-1}")"; [[ -z "$z" ]] && z=1
              readarray -t deal < <(deal      "$x" "$z")
              readarray -t "$x" < <(arraydiff "$x" deal)
              readarray -t "$y" < <(arrayadd  "$y" deal)
              echo -en "$(threechar <<< ${deal[@]})"
              echo -e  "  ⇒  $y";}
npeopleadd(){ readarray -d$' ' -t allppl < <(arrayadduq allppl "$@");}


# Selection
# ----------------------------------------------------------------------------
peopleselection() { select person in Everybody "${allppl[@]}"
                    do
                        case "$person" in
                            "Everybody") byline <<< "${allppl[@]}" ;;
                            *)           echo "$person"            ;;
                        esac
                        break
                    done;}
mainselection()   {
    options=("New deck" "Shuffle" "Add people"
             "Deal cards" "Show cards" "Quit")
    select option in "${options[@]}"
    do
        case "$option" in
            "New deck")   makedeck
                          echo "New deck is ready. Shuffle it." ;;
            #----------------------------------------------------------------
            "Shuffle")    nshuffle deck
                          echo "Shuffled."                      ;;
            #----------------------------------------------------------------
            "Add people") read -r -p "Names, separated by space: " newppl
                          newppl="$(sanitizeS "$newppl")"
                          npeopleadd newppl
                          echo "People:  $(whatis allppl)"      ;;
            #----------------------------------------------------------------
            "Deal cards") until [[ "$number" =~ ^[1-9][0-9]*$ ]]
                          do  read -r -p "How many cards? " number
                          done
                          echo "To whom?"
                          selec="$(peopleselection)"
                          while read -r per
                          do  ndeal deck "$per" "$number"
                          done <<< "$selec"
                          number=                               ;;
            #----------------------------------------------------------------
            "Show cards") echo "Whose cards?"
                          selec="$(peopleselection)"
                          width="$(wc -L <<< "$selec")"
                          count="$(wc -l <<< "$selec")"
                          while read -r per
                          do  printf "%-$((width+1))s\n" "$per"
                              sortgrp "$per"
                          done <<< "$selec" | column-it "$count";;
            #----------------------------------------------------------------
            "Quit")       echo "Goodbye!"; break                ;;
        esac
        echo
    done
}


# Main
# ----------------------------------------------------------------------------
main() { case "$1" in
             -h) helpme; exit 1 ;;
             *)  clear
                 echo "Welcome to cards"
                 echo "----------------"
                 declare -a suits values deck allppl options
                 makedeck
                 mainselection
         esac;}

main "$@"

exit 0