A very small challenge

This is a very minor piece of code,
really. But I’m not seeing the "right"
way to do it.

If you’re bored by cards in general
(as in card games), you can stop reading
here. :slight_smile:

I spent last night working on a little
program to play Spades (the card game).
It’s amazing how much actually works
after just a couple of hours and 158
lines of code, by the way. That’s Ruby.

I’m trying to write a little "winner"
method to determine which player (which
we can represent by an index 0-3) wins
the trick.

I’ve implemented this for now as a class
method taking a parameter which is an
array of four cards. Each card is an object
with accessors ‘suit’ and ‘rank’.

According to the rules of the game, the
suit that is led first must be followed.
The winner method isn’t/shouldn’t be
responsible for detecting breaches of the
rule; but it affects the “value” of all
the subsequent cards (e.g., if a two of
hearts led, it beats a king of clubs
following). On the other hand, the spade
suit is a trump which will beat anything.

So how would you do this?

Hal

“Hal E. Fulton” hal9000@hypermetrics.com writes:

I’ve implemented this for now as a class method taking a parameter
which is an array of four cards. Each card is an object with accessors
’suit’ and ‘rank’.

Possibly that’s what’s causing you a problem. Perhaps the concept of a
round of cards has enough intrinsic behavior to qualify for it’s own
class:

round  = Round.new

players.each do |p|
  card = p.select_card(round)  # I'm assuming the round provides
                               # context to guide the selection
  round.add(p, card)
end

winner = round.winner

or somesuch… :slight_smile:

Dave

Hal E. Fulton wrote:

This is a very minor piece of code,
really. But I’m not seeing the "right"
way to do it.

Surely, there is now “right” way, but the way you look at the decision
rule affects what you see.

Here are my thoughts:

The web page at http://www.pagat.com/boston/spades.html gives the
decision rule for a trick in spades as:

“A trick containing a spade is won by the highest spade played; if no
spade is played, the trick is won by the highest card of the suit led.”

With theses rules, the decision of higher card is a binary
relationship on cards and can be expressed as a boolean method of the
class card. For example card1.loosesto(card2), where loosesto() assumes
card1 is the best card on the trick so far. When the next player plays
card2 on the trick, card1 is asked if it can beat card2.

The logic for loosesto() goes like:

if (card2.suit == spades) then
if (self.suit == spades) then
return card2.rank > card1.rank
else
return true
end
else
if (card2.suit == self.suit) then
return card2.rank > card1.rank
else
return false
end
end

Using loosesto() the loop that scans the array of cards that encodes the
trick could read something like:

def decide_trick(trick_array)
current_best = trick_array[0]
index = 0
(1…3).each { |x|
if current_best.loosesto(trick_array[i]) then
current_best = trick_array[i]
index = i
end
}
return "player #{index} wins with card #{current_best}"
end

Your thoughts?

“Hal E. Fulton” hal9000@hypermetrics.com writes:

I’ve implemented this for now as a class method taking a parameter
which is an array of four cards. Each card is an object with
accessors ‘suit’ and ‘rank’.

Hal:

I’ve played with the idea of recording the winner in a Round
object. It seems fairly natural:

 class Card
   attr_reader :suit, :rank

   def initialize(suit, rank)
     @suit = suit
     @rank = rank
   end

   # return the better of this card and another
   def best_of(other)
     if @suit == other.suit
       @rank > other.rank ? self : other
     elsif other.suit == :spade
       other
     else
       self
     end
   end
 end

 # Record the best of a series of cards

 class Round
   attr_reader :winner
   def initialize
     @winner = nil
   end

   def add(card)
     if @winner
       @winner = @winner.best_of(card)
     else
       @winner = card
     end
   end
 end

Having said that, I’m not familiar with the game, so here are some
testcases which capture my assumptions of what I thought you meant :slight_smile:

 require "test/unit"

 class TestRound < Test::Unit::TestCase

   # Generate cards C2..C14, D2..D14, etc
   for suit in [:club, :diamond, :heart, :spade]
     for rank in 2..14
       name = "#{suit.to_s[0,1].upcase}#{rank}"
       const_set(name, Card.new(suit, rank))
     end
   end

   def testSimple
     r = Round.new
     r.add(H2)
     assert_equal(H2, r.winner)
     r.add(H8)
     assert_equal(H8, r.winner)
     r.add(H4)
     assert_equal(H8, r.winner)
   end

   def testMixed
     r = Round.new
     r.add(H2)
     assert_equal(H2, r.winner)
     r.add(C8)
     assert_equal(H2, r.winner)
     r.add(D4)
     assert_equal(H2, r.winner)
     r.add(H3)
     assert_equal(H3, r.winner)
   end

   def testSpade
     r = Round.new
     r.add(H8)
     assert_equal(H8, r.winner)
     r.add(C8)
     assert_equal(H8, r.winner)
     r.add(S4)
     assert_equal(S4, r.winner)
     r.add(H10)
     assert_equal(S4, r.winner)
     r.add(S5)
     assert_equal(S5, r.winner)
   end

   def testInitialSpade
     r = Round.new
     r.add(S2)
     assert_equal(S2, r.winner)
     r.add(C8)
     assert_equal(S2, r.winner)
     r.add(S4)
     assert_equal(S4, r.winner)
   end
 end

The logic for loosesto() goes like:

The opposite of “win” is “lose”.

Gavin :wink:

I’ve played with the idea of recording the winner in a Round
object. It seems fairly natural:
(some snips)
# return the better of this card and another
def best_of(other)
if @suit == other.suit
@rank > other.rank ? self : other
elsif other.suit == :spade
other
else
self
end
end
end

Yes! Thank you.

You’ve made a simplification here which is tricky enough
that I’ll point it out. You may even have done it semi-
unconsciously. :slight_smile:

I was thinking in terms of a “comparison” – compare the
value of this card with that one. But you’d need to know
what suit led. So I had confused myself. Not uncommon.

In a simple comparison, x and y have a relationship that’s
fixed without regard to ordering. I’m not making sense,
am I? :slight_smile:

But as long as you compare the cards in order from earliest
to latest, you don’t worry what suit led.

Why this might be considered odd can be seen by looking at
these two cases:

D6.best_of(C8) # C8
C8.best_of(D6) # D6 - different answers

In other words, the chronologically earlier one wins.

Since it is past 12:30 am right now (a dangerous time for
me to try to think) this still confuses me.

It’s as if the test “x > y” were true and “y > x” were also
true.

It’s not like integers, in other words. Imagine a method
like

class Fixnum
def best_of(other)
self > other ? self : other
end
end

Unless I’ve done something stupid, this means

6.best_of(4) # 6
4.best_of(6) # 6 - same answer

You’re probably shaking your head at my stating the
obvious, so I’ll shut up now.

Having said that, I’m not familiar with the game, so here are some
testcases which capture my assumptions of what I thought you meant :slight_smile:

(snip)

That’s precisely what I meant. Your assumptions are correct.

Cheers,
Hal

···

----- Original Message -----
From: “Dave Thomas” Dave@PragmaticProgrammer.com
To: “ruby-talk ML” ruby-talk@ruby-lang.org
Sent: Wednesday, August 07, 2002 10:19 PM
Subject: Re: A very small challenge

Replying again on this one.

You made the breakthrough for me. But I was thinking
in terms of true/false, and I also implemented the
cards a little differently.

The concept of a round as an object wasn’t compelling
enough to me personally. This is how I ended up
doing it.

class Card

(snip)

def beats(other)
if @suit == other.suit
Ranks.index(@rank) > Ranks.index(other.rank)
elsif @suit == "S"
true
else
false
end
end

end

class Driver

(snip)

def Driver.winner(cards)
high = cards[0]
cards[1…3].each {|card| high=card if card.beats(high) }
high
end
end

Ciao,
Hal

···

----- Original Message -----
From: “Dave Thomas” Dave@PragmaticProgrammer.com
To: “ruby-talk ML” ruby-talk@ruby-lang.org
Sent: Wednesday, August 07, 2002 10:19 PM
Subject: Re: A very small challenge

Gavin Sinclair wrote:

The logic for loosesto() goes like:

The opposite of “win” is “lose”.

Gavin :wink:

yes, and ‘no’ usually isn’t spelled with a ‘w’. guess i need a better
copy editor or a lot less cafeine.

John