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:
- to test the suitability of non-OOP approaches to a typical OOP problem, and
- 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