How to extend structs for equality, in order to make use of operator like `in`?

I’m creating a package to run Black Jack simulations.

I would like to write structs A, C2 (2-card), … etc for each card type in a deck. And, to have a general Card struct, so I can overload methods like +, ==, with Card logic that permeates to every card.

So far, I tried some things, and ended up with problems:

export A, C2, C3, C4, C5, C6, C7, C8, C9, C10, CJ, Card, Deck, NewDeck, Hand, isA, isTens, is2_6, CountPositive, CountNegative, CardType

struct Card
    Symbol::String
    Value::Vector{Int}
end

# Define a parametric struct for card types
struct CardType{T<:Card}
    card::T
end

@generated function ≂(x, y)
    if !isempty(fieldnames(x)) && x == y
        mapreduce(n -> :(x.$n ≂ y.$n), (a,b)->:($a && $b), fieldnames(x))
    else
        :(x == y)
    end
end

function ==(a::T, b::T) where T <: CardType
    if !isempty(fieldnames(x)) && x == y
        mapreduce(n -> :(x.$n ≂ y.$n), (a,b)->:($a && $b), fieldnames(x))
    else
        :(x == y)
    end
end

struct A
    card::Card
    function A()
        new(Card("A", [1,11]))
    end
end

struct C2
    card::Card
    function C2()
        new(Card("C2", [2]))
    end
end

In the REPL, we see that the structs don’t behave well with equality.

julia> c = C2()
C2(Card("C2", [2]))

julia> c2 = C2()
C2(Card("C2", [2]))


julia> c == c2
false

julia> c ≂ c2
true

julia> C2() in [C2(), C2()]
false

How could I tweak == in order to use in, and == as expected?

This doesn’t make much sense, since the only subtype of Card is Card (concrete types are final in Julia) — it is effectively equivalent to:

struct CardType
    card::Card
end

are you trying to make every card, e.g. A♡ or K♠ be a different type? I wouldn’t recommend this — if you have an array of cards, it will then be a heterogeneously-typed container and be slow.

function ==(a::T, b::T)

Did you forget to import == from Base in order to extend it?

This doesn’t make much sense either. There effectively only one CardType, so you will always have the same fields.

I’m pretty confused about what you want the data structure to represent and what you want == to do.

Example code

If you want to represent cards in a standard deck, why not just define a single Card type, e.g. something like:

@enum CardSuit::UInt8 ♠ ♣ ♡ ♢
const spade, club, heart, diamond = ♠,♣,♡,♢
const J, Q, K, A = UInt8.(11:14)
struct Card
    value::UInt8 # 2–10, 11=J, 12=Q, 13=K, 14=A
    suit::CardSuit
    function Card(value::Integer, suit::CardSuit)
        1 < value < 15 || throw(ArgumentError("card values must be between 2 and 14 (A)"))
        return new(value, suit)
    end
end

at which point the built-in == method will work as-is. It will also be nice to define some convenience methods for pretty-printing and construction by multiplication:

Base.:*(v::Integer, s::CardSuit) = Card(v, s)
function Base.show(io::IO, c::Card)
    if c.value < 11
        print(io, Int(c.value))
    else
        print(io, c.value == J ? 'J' : c.value == Q ? 'Q' : c.value == K ? 'K' : 'A', '*')
    end
    print(io, c.suit)
end

at which point you can do:

julia> 3heart
3♡

julia> A*♠
A*♠

julia> deck = [Card(v,s) for v in 2:14, s in (♠,♣,♡,♢)]
13×4 Matrix{Card}:
 2♠   2♣   2♡   2♢
 3♠   3♣   3♡   3♢
 4♠   4♣   4♡   4♢
 5♠   5♣   5♡   5♢
 6♠   6♣   6♡   6♢
 7♠   7♣   7♡   7♢
 8♠   8♣   8♡   8♢
 9♠   9♣   9♡   9♢
 10♠  10♣  10♡  10♢
 J*♠  J*♣  J*♡  J*♢
 Q*♠  Q*♣  Q*♡  Q*♢
 K*♠  K*♣  K*♡  K*♢
 A*♠  A*♣  A*♡  A*♢

julia> using Random

julia> shuffle(deck)
13×4 Matrix{Card}:
 3♠   4♣   K*♡  10♢
 10♣  J*♠  K*♢  7♠
 5♣   5♡   6♠   10♡
 2♢   7♡   J*♡  A*♡
 5♢   9♡   6♡   6♣
 A*♢  9♣   4♠   3♢
 8♣   4♡   3♡   6♢
 2♣   Q*♡  10♠  8♢
 2♡   8♠   2♠   5♠
 K*♠  K*♣  4♢   9♢
 J*♣  J*♢  3♣   Q*♢
 7♣   7♢   8♡   Q*♣
 9♠   A*♣  Q*♠  A*♠
3 Likes

Better yet, use an existing package that does this and more, e.g. PlayingCards.jl (which is much better than my toy code above, e.g. it only uses one byte per card, has many more features, and in general is better thought-through than my rough code above).

1 Like

Thanks for the initial reply, Steven. And, sharing the PlayingCards.jl package.

The goal is that I will have an struct for the Hand of Blackjack

mutable struct Hand
    InitialCards::Pair{Card, Card}
    Cards::Array{Card}
    Value::Int
    function Hand(InitialCards)
        # Value = sum(InitialCards)
    end
end

And I’m writing the sum function for the Hand, and one of the things I have to do is check if the hand has an Ace. Because, it can either be valued as an 11, or 1. For example, if I have an A,3 (14), then I hit and now I have A,3,8, my hand is valued 12, not 22.

So, I have to check if A() in Hand.Cards.

As I showed earlier, in my implementation, when I do A() in [A(), A()] it returns false. And, It should be true. This is important for the function-method sum.

I will work around PlayingCards.jl

It doesn’t implement some abstractions I would like, like a general Q(), regardless of suit. But, I can make extensions easily.

For example, I can work around that with:

julia> rank(PlayingCards.Q♠) in [rank(card) for card in cards]
true

julia> cards
3-element Vector{PlayingCards.Card}:
 Q♡
 K♡
 A♡

Then, I will implement BJ-card-values constrains logic, inside the game structs, like Hand. (J, Q, K and Ace) have different values, in Black Jack etc.

This seems like the wrong abstraction here, because an ace is not a single card — it is an equivalence class of possible cards (aces in four suits, A♣ etc.). Unless you don’t store the suit and just store the equivalence class (the card value).

Here is how you would check if an ace is in a hand using PlayingCards.jl:

julia> using PlayingCards

julia> deck = shuffle!(ordered_deck());

julia> hand = pop!(deck, 4)
(T♠, 3♠, A♣, Q♣)

julia> any(card -> rank(card) == 1, hand) # check for aces, in any suit
true

julia> any(card -> rank(card) == 4, hand) # check for a 4 in any suit
false

So, for example, to sum the blackjack value of a hand, you might do:

function blackjack_value(hand)
    value = n_aces = 0
    for card in hand
        r = rank(card)
        value += min(r, 10)
        n_aces += r == 1
    end
    while value < 12 && n_aces > 0
        value += 10 # treat aces as 11 if beneficial
        n_aces -= 1
    end
    return value
end

gives

julia> blackjack_value((K♢, 8♣, 2♣, A♠))
21

julia> blackjack_value((K♢, A♠))
21
4 Likes

Amazing, that took me a minute to understand the algorithm. It makes sense and it works.

Also, interesting use of any. I will keep it in mind and follow that construction.

Thanks, @stevengj

In case anyone is interested. I just made the repository I was working in public.

Feel free to contribute, if you feel like it.

Regards,
Pedro