2

I'm making a card hand generator, and I want it to note if a specific hand is generated, like a three of a kind or a full house. I was trying to figure out how to do it, and I thought of using grep on the strings, but I realized I would have to make a LOT of lines and it would be overly repetitive. There is a file that holds the last generated string called out.txt.

This is an output from the script:

 io@conroe$ ./card 5
   ♦ 6    ♦ Q    ♠Q    ♥J    ♣3

I would want something like this:

 io@conroe$ ./card 5
   ♦ 6    ♦ Q    ♠Q    ♥J    ♣3
   PAIR - QUEENS

The code is simple to me, but I'm still trying to wrap my head around how to do this part. (Now, what would be really cool is if I could find a set of small pixel card graphics and use them in place of the text.) Code:

#!/bin/bash
cat /dev/null > out.txt 
z=$( < out.txt)
for (( y=1; y<=$1; y++ ))
  do    
      < /dev/urandom LC_CTYPE=C tr -dc HDCS | head -c 1 | while read -n 1 s  
do
        case $s in
    D)
        printf '  \e[0;31;47m ♦ '
        ;;
    H)
        printf '  \e[0;31;47m ♥'
        ;;
    S)
        printf '  \e[0;30;47m ♠'
        ;;
    C)
        printf '  \e[0;31;47m ♣'
        ;;
esac


done
    < /dev/urandom LC_CTYPE=C tr -dc "1""2""3""4""5""6""7""8""9""10""J""Q""K""A" | head -c 1 | while read -n 1 n
do  
        if [ $n = "0" ]
    then
        echo -n '10 '
        echo -n '10 ' >> out.txt

        else
        echo -n "$n "
        echo -n "$n " >> out.txt

        fi

done
printf '\e[0m'
  done
  printf "\n"

EDIT: By the way, the strings in out.txt look like this: 6 Q Q J 3

4
  • Do you realise that you can deal the same card several times?
    – choroba
    Jun 23, 2015 at 22:00
  • Riiight, oops. How can I prevent that? Jun 23, 2015 at 22:02
  • out.txt contains no info about suits?
    – yaegashi
    Jun 24, 2015 at 0:55
  • It does now. It looks like this: m ♦ A m ♠ 2 m ♣ J m ♦ 7 m ♥ 2 Jun 24, 2015 at 1:01

4 Answers 4

4
printf '%s\n' {♠,♣,♢,♡}$'\t'{{2..10},J,K,Q,A} | shuf -n5 |
  gawk 'BEGIN{ split(",Twos,Threes,Fours,Fives,Sixes,Sevens,Eights,Nines,Tens",vt,","); vt["J"]="Jacks"; vt["Q"]="Queens"; vt["K"]="Kings"; vt["A"]="Aces"; } # values-text
        { c[$2]++; printf("%s %s", $1, $2(NR==5?"\n":"\t")) }
        END{ for(i in c){
                 if( c[i]==2 ){ print "PAIR:  " vt[i]; cp++ }  
                 if( c[i]==3 ){ print "THREE: " vt[i]; ct++ }
                 if( c[i]==4 ){ print "FOUR:  " vt[i] } }
             if( cp==2  ) { print "TWO PAIRS" }
             if( cp&&ct ) { print "FULL HOUSE" } }'

Example output:

♡ Q    ♣ A    ♢ A    ♢ Q    ♡ 2 
PAIR:  Aces
PAIR:  Queens
TWO PAIRS

Here is the same thing done entirely by awk, except for the method of seeding awk's rand(), by using bash's $RANDOM passed to awk via the -v option. The output is identical to the above.

gawk -v seed=$RANDOM '
  BEGIN{srand(seed) 
        split("♠♣♢♡",s,"")  # suit: 1-4 
        split("A,2,3,4,5,6,7,8,9,10,J,Q,K",v,",")  # values: 1-13
        split(",Twos,Threes,Fours,Fives,Sixes,Sevens,Eights,Nines,Tens",vt,","); vt["A"]="Aces"; vt["J"]="Jacks"; vt["Q"]="Queens"; vt["K"]="Kings"; # values-text
        for(es in s){ for(ev in v){ sv[i++]=s[es]" "v[ev] }}; # 0-51
        imax=4; for(i=0;i<=imax;i++){              # pick 5 cards at random from array `v`
          rix=int(rand()*(52-i))+i                 # ranges from 0-51 to 4-51, as per `i`
          tmp=sv[i]; sv[i]=sv[rix]; sv[rix]=tmp    # swap ramdom value to front of array, as per `i` 
          split(sv[i],fv," "); c[fv[2]]++          # increment face-value counts  
          printf("%s", sv[i](i==imax?"\n":"\t"))   # print the full hand in incremets
        }
        for(i in c){
            if( c[i]==2 ){ print "PAIR:  " vt[i]; cp++ }  
            if( c[i]==3 ){ print "THREE: " vt[i]; ct++ }
            if( c[i]==4 ){ print "FOUR:  " vt[i] } }
        if( cp==2  ) { print "TWO PAIRS" }
        if( cp&&ct ) { print "FULL HOUSE" }}'
2
  • This is really cool.
    – mikeserv
    Jun 24, 2015 at 4:46
  • @viktorahlström: Somewhere along the line, will all the edits I've made, I lost the THREE counter increment ct++ – fixed now
    – Peter.O
    Jun 25, 2015 at 6:21
2

I'd generate a list of all possible combinations, then shuffle it and pick the given number of cards (this prevents dealing the same card several times). To count the same suits and ranks, I'd use associative arrays.

This should get you started:

#!/bin/bash

count=$1
((count>52)) && exit 1 # Not enough cards.

cards=() # Create an array.
for suit in $'\e[0;31;47m ♦' $'\e[0;31;47m ♥' $'\e[0;30;47m ♠' $'\e[0;30;47m ♣' ; do
    for rank in {2..10} J Q K A ; do
        cards+=("$suit $rank"$' \e[0m') # All possible combinations.
    done
done

hand=($(
        for ((i=0; i<${#cards[@]}; i++)) ; do
            echo $i
        done | shuf -n "$count" # Pick random cards.
        ))

# Associative arrays to count occurrences.
declare -A suits
declare -A ranks
for card_i in "${hand[@]}" ; do
    card="${cards[card_i]}"
    echo "$card"
    (( suits[${card:11:1}]++ ))  # Extract the suit
    (( ranks[${card:13:-5}]++ )) # and rank, add one to count.
done

for s in "${!suits[@]}" ; do
    echo "$s ${suits[$s]}"
done 

echo

for r in "${!ranks[@]}" ; do
    echo "$r ${ranks[$r]}"
done 
2
  • @viktorahlström: What version of bash do you have?
    – choroba
    Jun 23, 2015 at 23:01
  • I'm on Mac, so it doesn't come packaged with shuf. Thankfully, I have brew so I installed it. I just removed the two lines and it works fine. If you need it, it's bash-3.2 Jun 24, 2015 at 1:03
2

Delayed answer to OP's comments. I used shuf like choroba's, but mine is encoding cards as integers from 0 to 51. Alternatively you can utilize $RANDOM variable for casual tests.

You can get this via https://gist.github.com/yaegashi/57065723166e3a72b79e

#!/bin/bash

# This version conforms to Bash 3.2.53 on OS X.

stab=('\e[0;31;47m ♦' '\e[0;31;47m ♥' '\e[0;30;47m ♠' '\e[0;30;47m ♣')
ntab=(2 3 4 5 6 7 8 9 10 J Q K A)

match() {
        for i; do
                printf "  ${stab[$((i/13))]}${ntab[$((i%13))]}\e[0m"
        done
        printf "\n"

        suits=(0 0 0 0)
        nums=(0 0 0 0 0 0 0 0 0 0 0 0 0)
        same2=()
        same3=()
        same4=()
        flush=-1
        straight=-1
        conseq=0
        for i; do
                ((suits[i/13]++))
                ((nums[i%13]++))
        done
        for ((i=0; i<4; i++)); do
                ((suits[i]==5)) && flush=$i
        done
        for ((i=0; i<13; i++)); do
                case ${nums[$i]} in
                2) same2+=($i) ;;
                3) same3+=($i) ;;
                4) same4+=($i) ;;
                esac
                if ((i>0)); then
                        conseq=$((nums[i]>0?(nums[i-1]>0?conseq+1:1):0))
                else
                        conseq=$((nums[i]>0?1:0))
                fi
                ((conseq==5)) && straight=$i
        done
        if ((${#same4[*]}>0)); then
                echo "FOUR OF A KIND - ${ntab[${same4[0]}]}"
        elif ((${#same3[*]}>0)); then
                if ((${#same2}>0)); then
                        echo "FULL HOUSE - ${ntab[${same3[0]}]} ${ntab[${same2[0]}]}"
                else
                        echo "THREE OF A KIND - ${ntab[${same3[0]}]}"
                fi
        elif ((${#same2[*]}>1)); then
                echo "TWO PAIR - ${ntab[${same2[1]}]} ${ntab[${same2[0]}]}"
        elif ((${#same2[*]}>0)); then
                echo "ONE PAIR - ${ntab[${same2[0]}]}"
        elif ((straight>=0)); then
                if ((flush>=0)); then
                        if ((straight==12)); then
                                echo "ROYAL FLUSH"
                        else
                                echo "STRAIGHT FLUSH"
                        fi
                else
                        echo "STRAIGHT"
                fi
        elif ((flush>=0)); then
                echo "FLUSH"
        else
                echo "NO PAIR"
        fi
}

# Tests
match 14 45 0 11 49  # NO PAIR
match 51 13 39 9 50  # ONE PAIR
match 34 21 1 11 50  # TWO PAIR
match 8 3 21 22 34   # THREE OF A KIND
match 51 24 36 9 21  # STRAIGHT
match 1 3 5 7 9      # FLUSH
match 5 18 31 15 28  # FULL HOUSE
match 10 9 22 35 48  # FOUR OF A KIND
match 1 2 3 4 5      # STRAIGHT FLUSH
match 12 11 10 9 8   # ROYAL FLUSH

# Random draw
#match $(shuf -e -n 5 {0..51})

shuf=({0..51})
cards=()
for i in 0 1 2 3 4; do
        j=$((RANDOM%${#shuf[*]}))
        cards+=(${shuf[$j]})
        unset shuf[$j]
done
match "${cards[@]}"

Output:

$ ./card.sh 
   ♥3   ♣8   ♦2   ♦K   ♣Q
NO PAIR
   ♣A   ♥2   ♣2   ♦J   ♣K
ONE PAIR - 2
   ♠10   ♥10   ♦3   ♦K   ♣K
TWO PAIR - K 10
   ♦10   ♦5   ♥10   ♥J   ♠10
THREE OF A KIND - 10
   ♣A   ♥K   ♠Q   ♦J   ♥10
STRAIGHT
   ♦3   ♦5   ♦7   ♦9   ♦J
FLUSH
   ♦7   ♥7   ♠7   ♥4   ♠4
FULL HOUSE - 7 4
   ♦Q   ♦J   ♥J   ♠J   ♣J
FOUR OF A KIND - J
   ♦3   ♦4   ♦5   ♦6   ♦7
STRAIGHT FLUSH
   ♦A   ♦K   ♦Q   ♦J   ♦10
ROYAL FLUSH
   ♠J   ♠4   ♥A   ♠6   ♣4
ONE PAIR - 4

Bug spottings, suggestions are welcome.

3
  • @viktorahlström Ok, revised.
    – yaegashi
    Jun 24, 2015 at 1:45
  • It wasn't on your part - it's the problem that I had bash-3.2, I updated to bash-4.3 and it runs successfully. Jun 24, 2015 at 1:46
  • I put the code on git, but I gave you credit for the help c: Jun 24, 2015 at 2:05
2

So you're definitely going wrong when you do:

tr -dc ... </dev/urandom

You don't need to -delete anything. If your goal is randomness - then you should use all you get.

For example:

tr '\0-\377' '[H*64][D*64][C*64][S*]' </dev/urandom |...

...which would always return one of [HDCS] w/out deleting any input, and would return it on a spread spectrum of random input bytes.

I wrote a function that will populate a shuffled deck:

deck()( HOME=/dev/null; ${deck:+"echo"}
        tr=$(printf '[%s*19]' 1 2 3 4 5 6 7 8 9 a b c d)
        tr '\0-\377' "[J*9]$tr"                 |
        dd cbs=1 obs=2 conv=unblock             |
        paste -d'W\nX\nY\nZ' - ~ - ~ - ~ - ~    |
        sed '   /^J/d;1!G;/^\(..\).*\1/d
                h;s/\n/&/51;tq' -e'd;:q' -eq
)       <"${1:-/dev/urandom}"

Doing...

time (deck|wc -l)

...prints...

52
( deck | wc -l; ) \
    0.03s user 0.04s system 224% cpu 0.028 total

deck() defaults to drawing its random data from the linux /dev/urandom PRNG, but if it is called with an argument it will interpret that as a filename for an alternate source of random input.

And every card returned - (one per line) - is unique. It doesn't bother trying to randomize the suits and assigns them in round-robin order. It doesn't have to bother: the order of the card values is already random, and the cards are going to have to be pruned for uniques in a random order anyway, and so the result of the pruning operation is random suits.

sed actually handles that. What sed does is:

  • /^J/d
    • clear all Jokers (byte values 0-$(((256%13)-1)))
  • 1!G
    • On every line but the first it Gets a copy of hold space appended to pattern space.
  • /^\(.*\)\n.*\1/d;h
    • If there is another card in its current stack which matches the card just pulled in it deletes pattern space and saves nothing...
    • ...else, if the current line is so far unique, it will copy the current stack to hold space.
    • The first line is always held.
  • /\(.*\n\)\{51\}/q;d
    • If there are 51 \newlines in pattern space at that time, sed quits input and prints the deck to stdout...
    • ...else it deletes pattern space and prints nothing.

Now if you wanted to draw...

draw()  if      [ -n "${1##*[!0-9]*}" ] || return 2
        then    case    $((${#deck}>($1*3)))$deck in
                (?*[!0-9W-Za-d[:space:]]*)
                        return 2;;
                (0*)    deck=$deck$(deck)
                        draw "$1";;
                (1*)    eval "  hand='$(echo "$deck" |
                                sed "$1 N;s/\n/' deck='/")'"
        esac;   fi

...which is a function that would automatically populate the current shell variables $hand and $deck as necessary. It pulls from the top of $deck the number of cards requested in $1 and puts those cards in $hand. $deck is trimmed from the top each time. If draw() is called and $deck is not sufficiently large enough to fill $hand as requested, then $deck is replenished with a new, shuffled $deck first.

And last:

show()  case    $1      in
        (*[!0-9W-Za-d[:space:]]*|'')    return 2;;
        (*) (   eval "  $(printf "T='\t' E=\033 nl='\n'")"
                str(){  m=$1 l=\$1$2 r=\$i$3 d=......
                        set 9 8 7 6 5 4 3 2 1
                        for i in d c b a "$@"
                        do      [ "$i" = d ] && M=ROYAL || M=STRAIGHT M=${M#"$m"}
                                eval printf "\"\ts/.*$l$d$r.*/"'$M${M:+ }$m:/;t$a.1\n"'
                                [ "$1" = 1 ] && unset a r l d i m M && break
                        shift;  done; }
                knd(){  c=$1    m="$1 OF A KIND" IFS=$nl
                        shift;  s="$*"; set -f .'\\1'
                        until   [ "$#" -gt "$((c-1))" ]
                                do set "$@" "$@"
                        done;   shift "$(($#-(c-1)))"
                        printf  %b "\t/\([a-d0-9]\)$@/{\n"
                        for     s in $s; do eval 'printf "\t\t%s\n" "'"$s\""; done
                        printf  "\tt$a.1\n\t}\n"; unset l c a s IFS; }
                br(){   case    $a.$1   in
                        (.*|*[!0-9]*.)  return 2;;
                        (*.-t)  printf  "\n:$a.0\n%s\n" \
                                        "$a!b${n:-$((a+1)).0}";;
                        (*.-b)  printf ":$a.1\n";;esac;shift
                        for s do eval 'printf "\t%s\n" "'"$s\"";done
                        unset n IFS a s; }
                for k in k1.2,1.2 k1.1,1.1 uk1.1,1.1
                do      echo "$1" | sort -"$k"; echo
                done|   sed -ne:n -e'$!N;s/\n\(.\)/\1/;tn
                                x;/./!g;x;$G;s/\n$//p'  |
                sed -ne"$(      a=1 br -t
                                a=1 str FLUSH   '\(.\)' '\\\1'
                                a=1 br -b       's/.*\([W-Z]\).......\\1.*/FLUSH:/' /:/h /^\[RS]/be n
                                a=2 br -t
                                a=2 knd 4       s/.\*/\$m:/
                                a=2 knd 3       s/// '/\(.\)[W-Z]\1/!s/.*[W-Z]/$m:/' s//FULLHOUSE:/          
                                a=2 knd 2       s///2 tP 's/.*/$m:/' :P 's/.*[W-Z]/2 PAIR:/'
                                a=2 br -b       /^\[F4]/h x //h //be x /:/h n
                                n=e a=3 br -t
                                a=3 str STRAIGHT .
                                a=3 br -b       /:/h x h)
                :e" -e'5!n;5!be' -e'y/123456789abcd/234567891JQKA/
                s/\(.\)\([W-Z]\)/ '"$E[0;3\2;47m \1 $E[m /g"'
                s/W\([^ ]* \)/1\1♦ /g;s/X\([^ ]* \)/1\1♥ /g
                s/Y\([^ ]* \)/0\1♠ /g;s/Z\([^ ]* \)/0\1♣ /g
                s/ 1/10/g;x;s/.*[^:]//;/.\{8\}/!s/$/'"$T/;G;s/\n/$T/
                s/\([^m]*m\)\{26\} /&\\$nl\\$nl$T$T/g;s/[[:space:]]*$//p"
)       esac

Throughout both of deck() and draw() the cards are stored in sort order like this:

1 2 3 4 5 6 7 8 9  a b c d
2 3 4 5 6 7 8 9 10 J Q K A

And the suits as well...

W X Y Z
♦ ♥ ♠ ♣

This makes it easy - every card is 2 bytes, and they'll always sort appropriately. The cards in $hand and $deck are all \newline delimited - always. So in show() the hardest part is done as simply as...

sort -k1.1,1.1

...where we sort the hand on the first byte of each line. The rest is just comparing - which sed does... a lot of. It handles 2,3,4 of a kind, fullhouse, 2 pair, royal straight, straight, royal flush, straight flush. It will prefer to report on them in the following order, regardless of how many cards are found in its first argument:

ROYAL FLUSH
STRAIGHT FLUSH
4 OF A KIND
FULLHOUSE
FLUSH
STRAIGHT
3 OF A KIND
2 PAIR
2 OF A KIND

Note that the show() function is not necessarily linked to either of the other two - it can be called with an argument generated in any way which coincides with the encoding scheme mentioned above and it will produce the output desired. Each of the functions can stand on its own in a modular way when/if needed.

Note also that there is no 5 card limit for any of the three functions - they should all handle hands of any size. And they are all designed to work a persistent $deck (with error checking) - and so can be used with some level of persistence.

It is at the very end of show() that the encoding is decoded. All encoded values are rendered decoded in a single action:

y/123456789abcd/234567891JQKA/

It happens at once in a single y/// translation without any danger of mistakenly editing the value twice.

Its output looks like:

enter image description here

9
  • Thanks for doing all of this. I had to tweak a bit of it, i.e I put HOME=/Users/root (my home directory, I put it as root because why not. the root user's directory is /var/root so no harm) at the end because HOME=/dev/null was set and it never output anything because it was still in /dev/null. I also had to prefix all linux coreutils with g because if you couldn't tell by the former, I'm on Mac, so I installed a coreutils package from brew, and all coreutils are prefixed with g on it. (tr to gtr and dd to gdd). Jun 25, 2015 at 2:02
  • The command takes an odd amount of time to run. Using time it says the command takes 6.89s. Jun 25, 2015 at 2:07
  • @viktorahlström - i know - i rewrote, it actually. To my shame i used some pathological regexps in show(). I should have relied on sort all the while. I also abstracted some of the sed into some script generative functions. It's a helluva lot easier on the eyes. I'll post it in a minute.
    – mikeserv
    Jun 25, 2015 at 3:23
  • @viktorahlström - the one I just posted will show "$deck" instantly.
    – mikeserv
    Jun 25, 2015 at 3:41
  • @viktorahlström - $HOME = /dev/null in the subshell - it is only to be able to do paste -d - ~ - ~ and mean paste -d - /dev/null - /dev/null - it's just a simple shortcut. And you really shouldn't need any coreutils for this to work either - it's all POSIX code. Admittedly some sed's do not adhere to spec where /\(match\)\{[num-repeat\}/ is concerned - they don't properly handle repeat intervals for submatches despite the spec clearly defining its intent there. I could make that a little more portable perhaps. Everything else should work fine. deck() won't w/out HOME=/dev/null.
    – mikeserv
    Jun 25, 2015 at 5:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.