# 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