Set, the card game

From Rosetta Code
Task
Set, the card game
You are encouraged to solve this task according to the task description, using any language you may know.
File:Fifteen set cards.jpg
twelve Set cards

The card game, Set, is played with a pack of 81 cards, each of which depicts either one, two, or three diamonds, ovals, or squiggles. The symbols are coloured red, green, or purple, and the shading is either solid, striped, or open. No two cards are identical.

In the game a number of cards are layed out face up and the players try to identify "sets" within the cards.

A set is three cards where either the symbols on the cards are all the same or they are all different, the number of symbols on the cards are all the same or all different, the colours are all the same or all different, and the shadings are all the same or all different.

For example, this is a set:

two solid green ovals
one open green squiggle
three striped green diamonds

because each card depicts a different symbol, the number of symbols on each card is different, the colours are all the same, and the shadings are all different.

This is not a set:

two solid purple ovals
one open green squiggle
three striped green diamonds

because two of the cards are green and one is purple, so the colours are neither all the same nor all different.

task
  • Create a representation of a pack of Set cards, shuffle it, select a specified number of cards from the pack and list them in the output.
  • Identify the sets in the selected cards and list them.
Also see

Acornsoft Lisp

See Set puzzle#Acornsoft_Lisp

The Set puzzle task is so similar that the same solution can be used. Just redefine play from

(defun play ((n-cards . 9))
  (find-enough-sets n-cards (quotient n-cards 2)))

to

(defun play ((n-cards . 9))
  (find-enough-sets n-cards 0))

C++

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <unordered_set>
#include <vector>

const std::vector<std::string> numbers { "ONE", "TWO", "THREE" };
const std::vector<std::string> colours { "GREEN", "RED", "PURPLE" };
const std::vector<std::string> shadings { "OPEN", "SOLID", "SRIPED" };
const std::vector<std::string> shapes { "DIAMOND", "OVAL", "SQUIGGLE" };

typedef std::vector<std::string> Card;

std::vector<Card> create_pack_of_cards() {
	std::vector<Card> pack;
	for ( std::string number : numbers ) {
		for  ( std::string colour : colours ) {
			for ( std::string shading : shadings ) {
				for ( std::string shape : shapes ) {
					Card card = { number, colour, shading, shape };
					pack.emplace_back(card);
				}
			}
		}
	}
	return pack;
}

bool all_same_or_all_different(const std::vector<Card>& triple, const int32_t& index) {
	std::unordered_set<std::string> triple_set;
	for ( const Card& card : triple ) {
		triple_set.insert(card[index]);
	}
	return triple_set.size() == 1 || triple_set.size() == 3;
}

bool is_game_set(const std::vector<Card>& triple) {
	return all_same_or_all_different(triple, 0) &&
		   all_same_or_all_different(triple, 1) &&
		   all_same_or_all_different(triple, 2) &&
		   all_same_or_all_different(triple, 3);
}

template <typename T>
std::vector<std::vector<T>> combinations(const std::vector<T>& list, const int32_t& choose) {
	std::vector<std::vector<T>> combinations;
	std::vector<uint64_t> combination(choose);
	std::iota(combination.begin(), combination.end(), 0);

	while ( combination[choose - 1] < list.size() ) {
		std::vector<T> entry;
		for ( const uint64_t& value : combination ) {
			entry.emplace_back(list[value]);
		}
		combinations.emplace_back(entry);

		int32_t temp = choose - 1;
		while ( temp != 0 && combination[temp] == list.size() - choose + temp ) {
			temp--;
		}
		combination[temp]++;
		for ( int32_t i = temp + 1; i < choose; ++i ) {
			combination[i] = combination[i - 1] + 1;
		}
	}
	return combinations;
}

int main() {
	std::random_device rand;
	std::mt19937 mersenne_twister(rand());

	std::vector<Card> pack = create_pack_of_cards();
	for ( const int32_t& card_count : { 4, 8, 12 } ) {
		std::shuffle(pack.begin(), pack.end(), mersenne_twister);
		std::vector<Card> deal(pack.begin(), pack.begin() + card_count);
		std::cout << "Cards dealt: " << card_count << std::endl;
		for ( const Card& card : deal ) {
			std::cout << "[" << card[0] << " " << card[1] << " " << card[2] << " " << card[3] << "]" << std::endl;
		}
		std::cout << std::endl;

		std::cout << "Sets found: " << std::endl;
		for ( const std::vector<Card>& combination : combinations(deal, 3) ) {
			if ( is_game_set(combination) ) {
				for ( const Card& card : combination ) {
					std::cout << "[" << card[0] << " " << card[1] << " " << card[2] << " " << card[3] << "] ";
				}
				std::cout << std::endl;
			}
		}
		std::cout << "-------------------------" << std::endl << std::endl;
	}
}
Output:
Cards dealt: 4
[TWO GREEN OPEN SQUIGGLE]
[THREE GREEN SRIPED OVAL]
[TWO RED SOLID SQUIGGLE]
[ONE GREEN SRIPED OVAL]

Sets found: 
-------------------------

Cards dealt: 8
[ONE PURPLE OPEN SQUIGGLE]
[ONE GREEN OPEN OVAL]
[ONE RED SOLID DIAMOND]
[TWO RED SOLID OVAL]
[TWO PURPLE OPEN DIAMOND]
[THREE PURPLE SOLID OVAL]
[TWO GREEN SOLID OVAL]
[THREE RED OPEN SQUIGGLE]

Sets found: 
[ONE GREEN OPEN OVAL] [TWO PURPLE OPEN DIAMOND] [THREE RED OPEN SQUIGGLE] 
-------------------------

Cards dealt: 12
[ONE RED SRIPED DIAMOND]
[THREE GREEN OPEN OVAL]
[ONE GREEN SRIPED DIAMOND]
[THREE GREEN SRIPED DIAMOND]
[TWO GREEN SRIPED SQUIGGLE]
[ONE PURPLE OPEN SQUIGGLE]
[TWO RED SOLID OVAL]
[ONE RED SOLID SQUIGGLE]
[THREE GREEN OPEN DIAMOND]
[THREE RED SRIPED DIAMOND]
[TWO RED OPEN OVAL]
[TWO PURPLE SRIPED SQUIGGLE]

Sets found: 
[THREE GREEN SRIPED DIAMOND] [ONE PURPLE OPEN SQUIGGLE] [TWO RED SOLID OVAL] 
[ONE PURPLE OPEN SQUIGGLE] [THREE GREEN OPEN DIAMOND] [TWO RED OPEN OVAL] 
[ONE RED SOLID SQUIGGLE] [THREE RED SRIPED DIAMOND] [TWO RED OPEN OVAL] 
-------------------------

Common Lisp

The Set puzzle task is so similar that the Common Lisp solution there could be used with only slight modification. Here we take a somewhat more different approach by creating the deck as a vector, so that it can be shuffled more efficiently, rather than taking a random sample from the deck represented as a list.

Compare Acornsoft Lisp above.

(defparameter numbers '(one two three))
(defparameter shadings '(solid open striped))
(defparameter colours '(red green purple))
(defparameter symbols '(oval squiggle diamond))

(defun play (&optional (n-cards 9))
  (let* ((deck (make-deck))
         (deal (take n-cards (shuffle deck)))
         (sets (find-sets deal)))
    (show-cards deal)
    (show-sets sets)))

(defun show-cards (cards)
  (format t "~D cards~%~{~(~{~10S~}~)~%~}~%"
          (length cards) cards))

(defun show-sets (sets)
  (format t "~D sets~2%~:{~(~@{~{~8S~}~%~}~)~%~}"
          (length sets) sets))

(defun find-sets (deal)
  (remove-if-not #'is-set (combinations 3 deal)))

(defun is-set (cards)
  (every #'feature-makes-set (transpose cards)))

(defun feature-makes-set (feature-values)
  (or (all-same feature-values)
      (all-different feature-values)))

(defun combinations (n items)
  (cond
    ((zerop n) '(()))
    ((null items) '())
    (t (append
          (mapcar (lambda (c) (cons (car items) c))
                  (combinations (1- n) (cdr items)))
          (combinations n (cdr items))))))

;;; Making a deck

(defun make-deck ()
  (let ((deck (make-array (list (expt 3 4))))
        (i -1))
    (dolist (n numbers deck)
      (dolist (sh shadings)
        (dolist (c colours)
          (dolist (sy symbols)
            (setf (svref deck (incf i))
                  (list n sh c sy))))))))

;;; Utilities

(defun shuffle (deck)
  (loop for i from (1- (length deck)) downto 0
        do (rotatef (elt deck i)
                    (elt deck (random (1+ i))))
        finally (return deck)))

(defun take (n seq)  ;  returns a list
  (loop for i from 0 below n
        collect (elt seq i)))

(defun all-same (values)
  (every #'eql values (rest values)))

(defun all-different (values)
  (every (lambda (v) (= (count v values) 1))
         values))

(defun transpose (list-of-rows)
  (apply #'mapcar #'list list-of-rows))
Output:

Depending on which Common Lisp you use, calling (play) might output:

12 cards
three     solid     purple    diamond   
one       striped   green     squiggle  
two       striped   purple    diamond   
three     open      purple    diamond   
three     striped   red       squiggle  
one       solid     green     squiggle  
three     open      purple    oval      
two       open      green     squiggle  
two       solid     red       diamond   
three     open      red       squiggle  
three     solid     red       oval      
two       solid     green     squiggle  

1 sets

one     striped green   squiggle
three   open    purple  oval    
two     solid   red     diamond 

EasyLang

attr$[][] &= [ "one  " "two  " "three" ]
attr$[][] &= [ "solid  " "striped" "open   " ]
attr$[][] &= [ "red   " "green " "purple" ]
attr$[][] &= [ "diamond " "oval    " "squiggle" ]
#
for card = 0 to 80
   pack[] &= card
.
proc card2attr card . attr[] .
   attr[] = [ ]
   for i to 4
      attr[] &= card mod 3 + 1
      card = card div 3
   .
.
proc prcards cards[] . .
   for card in cards[]
      card2attr card attr[]
      for i to 4
         write attr$[i][attr[i]] & " "
      .
      print ""
   .
   print ""
.
ncards = randint 5 + 7
print "Take " & ncards & " cards:"
for i to ncards
   ind = randint len pack[]
   cards[] &= pack[ind]
   pack[ind] = pack[len pack[]]
   len pack[] -1
.
prcards cards[]
#
for i to len cards[]
   card2attr cards[i] a[]
   for j = i + 1 to len cards[]
      card2attr cards[j] b[]
      for k = j + 1 to len cards[]
         card2attr cards[k] c[]
         ok = 1
         for at to 4
            s = a[at] + b[at] + c[at]
            if s <> 3 and s <> 6 and s <> 9
               # 1,1,1 2,2,2 3,3,3 1,2,3
               ok = 0
            .
         .
         if ok = 1
            print "Set:"
            prcards [ cards[i] cards[j] cards[k] ]
         .
      .
   .
.
Output:
Take 10 cards:
one   striped purple oval     
two   open    red    oval     
one   open    purple oval     
three open    purple diamond  
one   open    purple diamond  
three open    red    oval     
one   striped green  oval     
two   striped purple squiggle 
one   open    red    oval     
two   solid   green  oval     

Set:
one   striped purple oval     
three open    red    oval     
two   solid   green  oval     

Set:
two   open    red    oval     
three open    red    oval     
one   open    red    oval     

Factor

USING: grouping io kernel literals math.combinatorics
prettyprint qw random sequences sequences.product sets ;

CONSTANT: cards $[
    qw{
        one two three
        solid open striped
        red green purple
        diamond oval squiggle
    } 3 group <product-sequence>
]

: deal ( n -- seq ) cards swap sample ;

: set? ( seq -- ? ) cardinality { 1 3 } member? ;

: sets ( seq -- newseq )
    3 [ flip [ set? ] all? ] filter-combinations ;

: .length ( seq str -- ) write bl length . nl ;

: .cards ( seq -- )
    [ " " join dup "o" head? "" "s" ? append print ] each nl ;

: .sets ( seq -- )
    dup "Sets present:" .length [ .cards ] each ;

: play ( n -- )
    deal [ "Cards dealt:" .length ]
         [ .cards ]
         [ sets .sets ] tri ;

4 8 12 [ play ] tri@
Output:
Cards dealt: 4

two solid purple ovals
three open green diamonds
two striped purple ovals
three solid purple diamonds

Sets present: 0

Cards dealt: 8

two open red squiggles
one open red oval
two striped purple diamonds
one striped green oval
one striped red squiggle
three solid purple ovals
one solid green diamond
three striped purple ovals

Sets present: 1

two open red squiggles
one solid green diamond
three striped purple ovals

Cards dealt: 12

two striped purple diamonds
two open purple ovals
three striped green squiggles
one striped red diamond
three open green diamonds
three open green squiggles
two open green ovals
two solid red diamonds
three open purple squiggles
one open purple squiggle
two solid green ovals
two striped green ovals

Sets present: 2

one striped red diamond
three open purple squiggles
two solid green ovals

two open green ovals
two solid green ovals
two striped green ovals

FreeBASIC

Translation of: Phix
Dim Shared As String*5 nums(2) = {"one", "two", "three"}
Dim Shared As String*7 shades(2) = {"solid", "striped", "open"}
Dim Shared As String*6 colours(2) = {"red", "green", "purple"}
Dim Shared As String*8 symbols(2) = {" diamond", "    oval", "squiggle"}

Sub showcard(card As Integer)
    Dim As Integer n, s, c, m
    n = card Mod 3
    card \= 3
    s = card Mod 3
    card \= 3
    c = card Mod 3
    card \= 3
    m = card Mod 3
    Print Trim(nums(n)); "  "; Trim(shades(s)); "  "; Trim(colours(c)); "  "; _
    Trim(symbols(m)); Iif(n = 0, "", "s")
End Sub

Sub showsets(hand() As Integer)
    Dim As Integer i, j, k
    Dim As Integer uh = Ubound(hand) + 1
    Color 14: Print "Cards dealt: "; uh
    Color 7: Print
    If uh <> 81 Then
        For i = 0 To uh - 1
            showcard(hand(i))
        Next
    End If
    Dim As Integer sets = 0
    For i = 0 To uh - 3
        For j = i + 1 To uh - 2
            For k = j + 1 To uh - 1
                If (hand(i) + hand(j) + hand(k)) Mod 3 = 0 Then sets += 1
            Next
        Next
    Next
    Print
    Color 11: Print "Sets present: "; sets
    Color 7: Print
    If uh <> 81 Then
        For i = 0 To uh - 3
            For j = i + 1 To uh - 2
                For k = j + 1 To uh - 1
                    If (hand(i) + hand(j) + hand(k)) Mod 3 = 0 Then
                        showcard(hand(i))
                        showcard(hand(j))
                        showcard(hand(k))
                        Print
                    End If
                Next
            Next
        Next
    End If
End Sub

Randomize Timer
Dim As Integer i, deal, j
Dim As Integer pack(80)
For i = 0 To 80
    pack(i) = i
Next

For deal = 4 To 81 Step 4
    For i = 80 To 1 Step -1
        j = Int(Rnd * (i + 1))
        Swap pack(i), pack(j)
    Next
    Dim As Integer hand(deal - 1)
    For i = 0 To deal - 1
        hand(i) = pack(i)
    Next
    showsets(hand())
Next

Sleep

J

Implementation:

deck=: >,{;:each'one two three';'red green purple';'solid striped open';'diamond oval squiggle'
deal=: (?#) { ]

sets=:  {{ >;sets0\y }}
sets0=: {{ <;({:y) sets1\}:y }}
sets1=: {{ <~.;({:y) sets2 m\}:y }}
sets2=: {{ <(m,:n)<@,"2 1 y#~m isset n"1 y }}
isset=: {{ 1=#~.(m=n),(m=y),:n=y }}

disp=: <@(;:inv)"1

Task examples:

   four=: 4 deal deck
   eight=: 8 deal deck
   twelve=: 12 deal deck
   >disp four
one purple striped oval   
one purple striped diamond
three green solid oval    
two red solid oval        
   >disp eight
three green striped diamond  
two green open squiggle      
three purple striped squiggle
one purple solid squiggle    
two green solid oval         
three purple solid squiggle  
two red solid oval           
three purple solid diamond   
   >disp twelve
two red solid squiggle     
three purple open squiggle 
two purple open oval       
one purple open oval       
one green solid oval       
three green striped oval   
one green open oval        
three green open squiggle  
one red open squiggle      
one purple striped squiggle
two purple striped diamond 
two red open diamond       
   disp sets four
┌┐
││
└┘
   disp sets eight
┌┐
││
└┘
   disp sets twelve
┌─────────────────────────┬───────────────────────────┬──────────────────────────┐
three green open squiggleone purple striped squiggletwo red solid squiggle    
├─────────────────────────┼───────────────────────────┼──────────────────────────┤
one green open oval      two red open diamond       three purple open squiggle
├─────────────────────────┼───────────────────────────┼──────────────────────────┤
three green open squiggletwo red open diamond       one purple open oval      
└─────────────────────────┴───────────────────────────┴──────────────────────────┘

Java

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public final class SetTheCardGame {

	public static void main(String[] args) {
		List<Card> pack = createPackOfCards();
		for ( int cardCount : List.of( 4, 8, 12 ) ) {
		    Collections.shuffle(pack);
		    List<Card> deal = pack.subList(0, cardCount);
		    System.out.println("Cards dealt: " + cardCount);
		    for ( Card card : deal ) {
		    	System.out.println(card);
		    }		   
		    System.out.println();
		    
		    System.out.println("Sets found: ");		    
		    for ( List<Card> combination : combinations(deal, 3) ) {
		    	if ( isGameSet(combination) ) {
		    		for ( Card card : combination ) {
		    			System.out.print(card + " ");
		    		}
		    		System.out.println();
		    	}
		    }
		    System.out.println("-------------------------" + System.lineSeparator());
		}
	}
	
	private static interface Feature {}
	
	private static enum Number implements Feature { ONE, TWO, THREE }
	private static enum Colour implements Feature { GREEN, RED, PURPLE }
	private static enum Shading implements Feature { OPEN, SOLID, STRIPED }
	private static enum Shape implements Feature { DIAMOND, OVAL, SQUIGGLE }	
	
	private static record Card(Number number, Colour colour, Shading shading, Shape shape) {
		
		public String toString() {
			return "[" + number + " " + colour + " " + shading + " " + shape + "]";
		}
		
	}
	
	private static List<Card> createPackOfCards() {
		List<Card> pack = new ArrayList<Card>(81);
		for ( Number number : Number.values() ) {
			for  ( Colour colour : Colour.values() ) {
				for ( Shading shading : Shading.values() ) {
					for ( Shape shape : Shape.values() ) {
						pack.add( new Card(number, colour, shading, shape) );
					}
				}
			}
		}	
		return pack;
	}
	
	private static boolean isGameSet(List<Card> triple) {
		return allSameOrAllDifferent(triple.stream().map( c -> (Feature) c.number ).toList()) &&
			   allSameOrAllDifferent(triple.stream().map( c -> (Feature) c.colour ).toList()) &&
			   allSameOrAllDifferent(triple.stream().map( c -> (Feature) c.shading ).toList()) &&
			   allSameOrAllDifferent(triple.stream().map( c -> (Feature) c.shape ).toList());
	}
	
	private static boolean allSameOrAllDifferent(List<Feature> features) {
		Set<Feature> featureSet = new HashSet<Feature>(features);
		return featureSet.size() == 1 || featureSet.size() == 3;
	}	
	
	private static <T> List<List<T>> combinations(List<T> list, int choose) {
		List<List<T>> combinations = new ArrayList<List<T>>();
	    List<Integer> combination = IntStream.range(0, choose).boxed().collect(Collectors.toList());	
	    while ( combination.get(choose - 1) < list.size() ) {   	
	        combinations.add(combination.stream().map( i -> list.get(i) ).toList());	
	        int temp = choose - 1;
	        while ( temp != 0 && combination.get(temp) == list.size() - choose + temp ) {
	            temp -= 1;
	        }
	        combination.set(temp, combination.get(temp) + 1);
	        for ( int i = temp + 1; i < choose; i++ ) {
	        	combination.set(i, combination.get(i - 1) + 1);
	        }
	    }	
	    return combinations;
	}

}
Output:
Cards dealt: 4
[TWO RED OPEN SQUIGGLE]
[THREE PURPLE OPEN DIAMOND]
[TWO PURPLE STRIPED DIAMOND]
[THREE RED STRIPED DIAMOND]

Sets found: 
-------------------------

Cards dealt: 8
[ONE RED STRIPED SQUIGGLE]
[TWO RED SOLID OVAL]
[ONE PURPLE STRIPED OVAL]
[TWO GREEN OPEN SQUIGGLE]
[TWO GREEN STRIPED DIAMOND]
[THREE GREEN OPEN OVAL]
[TWO RED OPEN SQUIGGLE]
[ONE PURPLE SOLID SQUIGGLE]

Sets found: 
[TWO RED SOLID OVAL] [ONE PURPLE STRIPED OVAL] [THREE GREEN OPEN OVAL] 
-------------------------

Cards dealt: 12
[TWO PURPLE SOLID SQUIGGLE]
[TWO GREEN SOLID SQUIGGLE]
[THREE PURPLE OPEN DIAMOND]
[ONE RED SOLID DIAMOND]
[ONE PURPLE STRIPED OVAL]
[ONE PURPLE OPEN SQUIGGLE]
[TWO RED OPEN DIAMOND]
[THREE RED SOLID OVAL]
[THREE PURPLE SOLID SQUIGGLE]
[ONE GREEN STRIPED OVAL]
[ONE RED OPEN SQUIGGLE]
[ONE PURPLE OPEN OVAL]

Sets found: 
[TWO PURPLE SOLID SQUIGGLE] [THREE PURPLE OPEN DIAMOND] [ONE PURPLE STRIPED OVAL] 
[ONE RED SOLID DIAMOND] [ONE PURPLE OPEN SQUIGGLE] [ONE GREEN STRIPED OVAL] 
[TWO RED OPEN DIAMOND] [THREE PURPLE SOLID SQUIGGLE] [ONE GREEN STRIPED OVAL] 
-----------------------

Julia

import Random: shuffle
import Combinatorics: combinations

const NUMBERS = ["one", "two", "three"]
const SHADINGS = ["solid", "striped", "open"]
const COLORS = ["red", "green", "purple"]
const SYMBOLS = ["diamond", "oval", "squiggle"]

struct SetCard
    t::Tuple{UInt8, UInt8, UInt8, UInt8}
    function SetCard(num, sha, col, sym)
        @assert all(i -> 1 <= i <= 3, (num, sha, col, sym))
        return new(tuple(num, sha, col, sym))
    end
end

function Base.string(s::SetCard) 
    return "(" *
           join([NUMBERS[s.t[1]], SHADINGS[s.t[2]], COLORS[s.t[3]], SYMBOLS[s.t[4]]], " ") *
           (s.t[1] == 1 ? "" : "s") * ")"
end
Base.print(io:: IO, sc::SetCard) = print(io, string(sc))
Base.print(io:: IO, vsc::Vector{SetCard}) = print(io, "[" * join(string.(vsc), ", ") * "]")

"""  Return an iterator for a vector of the sets found in the dealt `cards` """    
function process_deal(cards::Vector{SetCard})
    return Iterators.filter(combinations(cards, 3)) do c
        return all(i -> (c[1].t[i] + c[2].t[i] + c[3].t[i]) % 3 == 0, eachindex(c[1].t))
    end
end

function testcardsets()
    pack = vec([SetCard(n, sh, c, sy) for n in 1:3, sh in 1:3, c in 1:3, sy in 1:3])
    numcards = 81
    while !isnothing(numcards)
        print("\n\nEnter number of cards to deal (3 to 81, or just a space to exit) => ")
        numcards = tryparse(Int, readline())
        if !isnothing(numcards) && 3 <= numcards <= 81
            deal = shuffle(pack)[begin:numcards]
            sets = collect(process_deal(deal))
            println("\nThe deal is:\n$deal\n\nThere are $(length(sets)) sets.")
            foreach(println, sets)
        end
    end
end

testcardsets()
Output:
Enter number of cards to deal (3 to 81, or just a space to exit) => 4

The deal is:
[(one striped red squiggle), (one striped purple diamond), (three solid purple diamonds), (three open red ovals)]

There are 0 sets.


Enter number of cards to deal (3 to 81, or just a space to exit) => 12

The deal is:
[(one striped green squiggle), (one solid green oval), (one open green oval), (one striped red diamond), (one open purple oval), (two open purple squiggles), (one solid red diamond), (three open purple diamonds), (three open green diamonds), (three striped red ovals), (three open green ovals), (two open red ovals)]

There are 3 sets.
[(one striped green squiggle), (one open purple oval), (one solid red diamond)]
[(one open purple oval), (two open purple squiggles), (three open purple diamonds)]
[(one open purple oval), (three open green ovals), (two open red ovals)]


Enter number of cards to deal (3 to 81, or just a space to exit) => 16

The deal is:
[(three open purple squiggles), (one open red diamond), (three striped purple diamonds), (three solid red diamonds), (one solid purple diamond), (one solid green oval), (three solid red ovals), (three solid purple ovals), (one solid green diamond), (two solid green diamonds), (two open purple squiggles), (one open purple squiggle), (three striped purple squiggles), (two solid green squiggles), (three solid red squiggles), (one open purple diamond)]

There are 6 sets.
[(three open purple squiggles), (three striped purple diamonds), (three solid purple ovals)]
[(three open purple squiggles), (two open purple squiggles), (one open purple squiggle)]
[(one open red diamond), (three striped purple diamonds), (two solid green diamonds)]
[(three solid red diamonds), (one solid purple diamond), (two solid green diamonds)]
[(three solid red diamonds), (three solid red ovals), (three solid red squiggles)]
[(one solid purple diamond), (three solid red ovals), (two solid green squiggles)]


Enter number of cards to deal (3 to 81, or just a space to exit) =>

Phix

Cards are 0..80 in decimal, which is 0000..2222 in base 3, and we can just subtract '/' from each digit to get indexes 1..3 to the constants.

with javascript_semantics
constant nums = {"one", "two", "three"},
       shades = {"solid", "striped", "open"},
      colours = {"red", "green", "purple"},
      symbols = {"diamond", "oval", "squiggle"}

procedure showcard(integer card)
    -- aside: &-1 prevents "JS does not support string subscript destructuring"
    integer {n,s,c,m} = sq_sub(sprintf("%04a",{{3,card}}),'/') & -1
    printf(1,"%s %s %s %s%s\n",{nums[n],shades[s],colours[c],symbols[m],iff(n=1?"":"s")})
end procedure

procedure showsets(sequence hand)
    integer lh = length(hand)
    printf(1,"Cards dealt: %d\n%n",{lh,lh!=81})
    if lh!=81 then papply(hand,showcard) end if
    sequence sets = {}
    for t in combinations(hand,3) do
        integer r3 = 0
        for r in {1,3,9,27} do
            r3 += rmdr(sum(sq_rmdr(sq_floor_div(t,r),3)),3)
        end for
        if r3=0 then sets = append(sets,t) end if
    end for
    printf(1,"\nSets present: %d\n\n",length(sets))
    if lh!=81 then
        for s in sets do
            papply(s,showcard)
            printf(1,"\n")
        end for
    end if
end procedure
        
sequence pack = tagstart(0,81)
for deal in {4,8,12,81} do
    pack = shuffle(pack)
    showsets(pack[1..deal])
end for
Output:
Cards dealt: 4

three open purple ovals
two solid green ovals
three solid red squiggles
three striped purple diamonds

Sets present: 0

Cards dealt: 8

two striped purple squiggles
three striped red squiggles
one striped green squiggle
two open purple diamonds
three solid green squiggles
two solid green squiggles
one striped purple oval
two solid purple squiggles

Sets present: 1

three striped red squiggles
one striped green squiggle
two striped purple squiggles

Cards dealt: 12

two open green diamonds
two striped purple diamonds
two open purple ovals
two solid red ovals
three solid purple squiggles
three striped green ovals
three solid green diamonds
one striped purple diamond
three solid green ovals
one open purple oval
three solid red diamonds
three solid purple ovals

Sets present: 5

three solid red diamonds
two open green diamonds
one striped purple diamond

three solid red diamonds
three solid green ovals
three solid purple squiggles

one striped purple diamond
two open purple ovals
three solid purple squiggles

two striped purple diamonds
one open purple oval
three solid purple squiggles

two solid red ovals
three striped green ovals
one open purple oval

Cards dealt: 81

Sets present: 1080

Python

from itertools import combinations
from itertools import product
from random import shuffle
from typing import Iterable
from typing import List
from typing import NamedTuple
from typing import Tuple

NUMBERS = ("one", "two", "three")
SHAPES = ("diamond", "squiggle", "oval")
SHADING = ("solid", "striped", "open")
COLORS = ("red", "green", "purple")


class Card(NamedTuple):
    number: str
    shading: str
    color: str
    shape: str

    def __str__(self) -> str:
        s = " ".join(self)
        if self.number != "one":
            s += "s"
        return s


Cards = List[Card]


def new_deck() -> Cards:
    """Return a new shuffled deck of 81 unique cards."""
    deck = [Card(*features) for features in product(NUMBERS, SHADING, COLORS, SHAPES)]
    shuffle(deck)
    return deck


def deal(deck: Cards, n: int) -> Tuple[Cards, Cards]:
    """Return _n_ cards from the top of the deck and what remains of the deck."""
    return deck[:n], deck[n:]


def is_set(cards: Tuple[Card, Card, Card]) -> bool:
    """Return _True_ if _cards_ forms a set."""
    return (
        same_or_different(c.number for c in cards)
        and same_or_different(c.shape for c in cards)
        and same_or_different(c.shading for c in cards)
        and same_or_different(c.color for c in cards)
    )


def same_or_different(features: Iterable[str]) -> bool:
    """Return _True_ if _features_ are all the same or all different."""
    return len(set(features)) in (1, 3)


def print_sets_from_new_deck(n: int) -> None:
    """Display sets found in _n_ cards dealt from a new shuffled deck."""
    table, _ = deal(new_deck(), n)
    print(f"Cards dealt: {n}\n")
    print("\n".join(str(card) for card in table), end="\n\n")

    sets = [comb for comb in combinations(table, 3) if is_set(comb)]
    print(f"Sets present: {len(sets)}\n")
    for _set in sets:
        print("\n".join(str(card) for card in _set), end="\n\n")

    print("----")


if __name__ == "__main__":
    for n in (4, 8, 12):
        print_sets_from_new_deck(n)
Output:
Cards dealt: 4

two open green diamonds
three striped green ovals
two open purple ovals
two open red squiggles

Sets present: 1

two open green diamonds
two open purple ovals
two open red squiggles

----
Cards dealt: 8

three striped purple diamonds
one solid purple oval
two open purple diamonds
three solid purple diamonds
one solid green squiggle
three open green squiggles
three open purple squiggles
three solid purple ovals

Sets present: 1

three striped purple diamonds
three open purple squiggles
three solid purple ovals

----
Cards dealt: 12

two open green squiggles
three solid purple ovals
three open red diamonds
two open red squiggles
three open purple ovals
three open red squiggles
three striped red squiggles
two open purple diamonds
three solid red squiggles
one solid red squiggle
two striped purple diamonds
one solid red diamond

Sets present: 2

two open red squiggles
three striped red squiggles
one solid red squiggle

three open red squiggles
three striped red squiggles
three solid red squiggles

----

Quackery

Why does isset, the word that tests if three cards constitute a set, use + and mod?

If we map any of the properties, say colour, onto the numbers 0, 1 and 2, then the sum of three colours mod 3 is 0 if and only if all the colours are different or all the colours are the same. This can be confirmed exhaustively, or for the underlying mathematics see the first two paragraphs of the section "A Mathematical Perspective" (pages 7 and 8) in this paper:

SETs and Anti-SETs: The Math Behind the Game of SET, by Charlotte Chan

transpose is defined at Matrix transposition#Quackery.

comb and arrange are defined at Combinations#Quackery.

  [ true swap transpose witheach
      [ 0 swap witheach +
        3 mod if [ not conclude ] ] ]         is isset    ( [ --> b )

  [ [ [] 81 times
      [ i 4 times [ 3 /mod swap ]
        drop 3 times join
        nested join ] ] constant
    shuffle swap split drop ]                 is cards    ( n --> [ )

  [ [] swap dup size swap 3 rot comb
    witheach
      [ dip dup arrange
        dup isset iff
          [ nested rot join swap ]
        else drop ] drop ]                    is sets     ( [ --> [ )

  [ unpack dup dip
      [ [ table
          $ "one" $ "two" $ "three" ]
        do echo$ sp
        [ table
          $ "solid" $ "striped" $ "open" ]
        do echo$ sp
        [ table
          $ "red" $ "green" $ "purple" ]
        do echo$ sp
        [ table
          $ "diamond" $ "squiggle" $ "oval" ]
        do echo$ ]
    if [ say "s" ] cr ]                       is echocard ( [ -->   )

  [ dup cards swap
    cr say "Cards dealt: " echo cr cr
    dup witheach echocard cr
    sets dup size
    say "Sets present: " echo cr cr
    witheach [ witheach echocard cr ] ]       is play     ( n -->   )

  ' [ 4 8 12 ] witheach [ play say "-----" ]
Output:
Cards dealt: 4

two striped green squiggles
one open purple oval
one solid purple diamond
three open red diamonds

Sets present: 0

-----
Cards dealt: 8

three open purple squiggles
two open purple ovals
three solid purple ovals
three solid red squiggles
two striped purple diamonds
two solid green squiggles
one striped green oval
one open purple diamond

Sets present: 1

three open purple squiggles
two open purple ovals
one open purple diamond

-----
Cards dealt: 12

one solid green diamond
one striped red diamond
one open purple squiggle
two solid green diamonds
two striped green squiggles
two solid red ovals
two solid green squiggles
one open green squiggle
two solid green ovals
two solid red diamonds
one open purple diamond
three striped purple diamonds

Sets present: 3

two solid red ovals
one open green squiggle
three striped purple diamonds

two solid green diamonds
two solid green squiggles
two solid green ovals

one solid green diamond
one striped red diamond
one open purple diamond

-----

Raku

my @attributes = <one two three>, <solid striped open>, <red green purple>, <diamond oval squiggle>;

sub face ($_) { .polymod(3 xx 3).kv.map({ @attributes[$^k;$^v] }) ~ ('s' if $_%3) }

sub sets (@cards) { @cards.combinations(3).race.grep: { !(sum ([Z+] $_».polymod(3 xx 3)) »%» 3) } }

for 4,8,12 -> $deal {
    my @cards = (^81).pick($deal);
    my @sets = @cards.&sets;
    say "\nCards dealt: $deal";
    for @cards { put .&face };
    say "\nSets found: {+@sets}";
    for @sets { put .map(&face).join("\n"), "\n" };
}

say "\nIn the whole deck, there are {+(^81).&sets} sets.";
Sample output:
Cards dealt: 4
one open purple squiggle
one striped red squiggle
three striped green diamonds
one open green diamond

Sets found: 0

Cards dealt: 8
three striped purple squiggles
three open green diamonds
one striped purple oval
three open red squiggles
two striped red diamonds
one solid purple diamond
one solid red oval
one solid green diamond

Sets found: 2
three open green diamonds
two striped red diamonds
one solid purple diamond

three open red squiggles
two striped red diamonds
one solid red oval

Cards dealt: 12
three open purple squiggles
one striped purple diamond
two striped red squiggles
two striped green squiggles
one solid green oval
three open red squiggles
two striped purple diamonds
three striped purple squiggles
one open red diamond
two striped red diamonds
two striped green ovals
one open green oval

Sets found: 3
three open purple squiggles
one solid green oval
two striped red diamonds

two striped red squiggles
two striped purple diamonds
two striped green ovals

one solid green oval
three open red squiggles
two striped purple diamonds


In the whole deck, there are 1080 sets.

Ruby

ATTRIBUTES = [:number, :shading, :colour, :symbol]
Card    = Struct.new(*ATTRIBUTES){ def to_s = values.join(" ") }
combis  = %i[one two three].product(%i[solid striped open], %i[red green purple], %i[diamond oval squiggle])
PACK = combis.map{|combi| Card.new(*combi) }

def set?(trio) = ATTRIBUTES.none?{|attr| trio.map(&attr).uniq.size == 2 }

[4, 8, 12].each do |hand_size|
  puts "#{"_"*40}\n\nCards dealt: #{hand_size}"
  puts hand = PACK.sample(hand_size)
  sets = hand.combination(3).select{|h| set? h }
  puts "\n#{sets.size} sets found"
  sets.each{|set| puts set, ""}
end
Sample output:
________________________________________

Cards dealt: 4
one striped green squiggle
one open red squiggle
two striped green oval
three solid green diamond

0 sets found
________________________________________

Cards dealt: 8
three open green squiggle
three striped green diamond
three striped red oval
three open red oval
three solid red diamond
one solid purple diamond
two open red diamond
one striped red diamond

2 sets found
three striped green diamond
one solid purple diamond
two open red diamond

three solid red diamond
two open red diamond
one striped red diamond

________________________________________

Cards dealt: 12
one solid purple oval
one striped purple oval
one open red diamond
three striped purple squiggle
three striped purple oval
three solid green squiggle
three solid purple diamond
two solid green squiggle
two open green squiggle
three open green diamond
two open purple squiggle
one striped red squiggle

3 sets found
one striped purple oval
three solid purple diamond
two open purple squiggle

one open red diamond
three striped purple oval
two solid green squiggle

three solid green squiggle
two open purple squiggle
one striped red squiggle


Wren

Library: Wren-ioutil
Library: Wren-fmt
Library: Wren-perm

Note that entering 81 for the number of cards to deal confirms that there are 1080 possible sets.

import "random" for Random
import "./ioutil" for Input
import "./fmt" for Fmt
import "./perm" for Comb

var nums = ["one", "two", "three"]
var shas = ["solid", "striped", "open"]
var cols = ["red", "green", "purple"]
var syms = ["diamond", "oval", "squiggle"]

var pack = List.filled(81, null)
var i = 0
for (num in 0..2) {
    for (sha in 0..2) {
        for (col in 0..2) {
            for (sym in 0..2) {
                pack[i] = [nums[num], shas[sha], cols[col], syms[sym]]
                i = i + 1
            }
        }
    }
}

var printCards = Fn.new { |cards|
    for (card in cards) {
        var pl = card[0] != "one" ? "s" : ""
        Fmt.print("$s $s $s $s$s", card[0], card[1], card[2], card[3], pl)
    }
}

var findSets = Fn.new { |cards|
    var sets = []
    var trios = Comb.list(cards, 3)
    for (trio in trios) {
        var t1 = trio[0]
        var t2 = trio[1]
        var t3 = trio[2]
        var found = true
        for (i in 0..3) {
            if (t1[i] == t2[i] && t2[i] == t3[i]) continue
            if (t1[i] != t2[i] && t2[i] != t3[i] && t1[i] != t3[i]) continue
            found = false
            break
        }
        if (found) sets.add(trio)
    }
    Fmt.print("Sets present: $d\n", sets.count)
    if (sets.count > 0) {
        for (set in sets) {
            printCards.call(set)
            System.print()
        }
    }
}

var prompt = "Enter number of cards to deal - 3 to 81 or q to quit: "
Input.quit = "q"
while(true) {
    Random.new().shuffle(pack) // shuffle for each deal
    var i = Input.integer(prompt, 3, 81)
    if (i == Input.quit) return
    var dealt = pack[0...i]
    System.print()
    printCards.call(dealt)
    System.print()
    findSets.call(dealt)
}
Output:

Sample run:

Enter number of cards to deal - 3 to 81 or q to quit: 4

three solid green diamonds
one solid red diamond 
one solid green oval 
three striped purple squiggles

Sets present: 0

Enter number of cards to deal - 3 to 81 or q to quit: 8

one open green squiggle 
one open purple squiggle 
one solid green squiggle 
three solid purple squiggles
three open green squiggles
one striped red diamond 
one striped green oval 
one striped green squiggle 

Sets present: 1

one open green squiggle 
one solid green squiggle 
one striped green squiggle 

Enter number of cards to deal - 3 to 81 or q to quit: 12

three open green ovals
three striped green diamonds
one solid purple oval 
one striped purple diamond 
two open green diamonds
three solid red diamonds
three solid red ovals
three solid green diamonds
three striped red ovals
three striped red squiggles
two open red squiggles
one solid green oval 

Sets present: 3

three striped green diamonds
one solid purple oval 
two open red squiggles

one solid purple oval 
two open green diamonds
three striped red squiggles

one striped purple diamond 
two open green diamonds
three solid red diamonds

Enter number of cards to deal - 3 to 81 or q to quit: q