Automating the Online Set Card Game

The Game

There’s a card game here that’s pretty fun to play. The challenge is to find all 6 sets of cards that make up a “set”. Rules can be found in this pdf.

Each day they have a puzzle that you can solve to find the 6 sets.

If you sign up for a free account, you can submit your name into a weekly drawing for a Set card game. Like a real one you can touch with your hands and stuff.

I like free stuff. And I like automating stuff!

Manual First, Then Automate

Before trying to automate anything though, it’s a good idea to run through the process manually.

See if you can find some sets. What happens when you find a set vs. what happens when the cards you pick aren’t actually a set? That’ll be useful info to know about later.

Once you know how the game works, then it’s worth trying to automate. Let’s give it a shot.

The Toolbox

The tool of choice is Watir, which is a Ruby library for interacting with web pages. If you’re familiar with Selenium, you’re already ahead–all Watir does is put some test tools in with the stuff that interacts with elements on the webpage.

Boilermaker

Let’s start by putting in the code to pull the appropriate gems at the top of the script:

require "rubygems"
require "watir-webdriver"

And if you haven’t installed these gems yet, just install watir from the command line by typing “gem install watir”. All the other dependencies related to Watir will be installed too. rubygems is part of a standard Ruby install.

Next, we need some boilerplate for opening up a browser and navigating to a url:

def setup
  client = Selenium::WebDriver::Remote::Http::Default.new
  client.timeout = 600
  # set up the $b component with the info here. 
  $b = Watir::Browser.new :ff, :http_client => client
  # $b = Watir::Browser.new :chrome, :http_client => client
  $timeout_length = 30
end

def go_to_url(url)
  load_link($timeout_length){ $b.goto url }
end

def load_link(waittime)
  begin
    Timeout::timeout(waittime)  do
      yield
    end
  rescue Timeout::Error => e
    log("Page load timed out: #{e}")
    retry
  end
end

For the final bit of boilerplate, we need something to close out the browser when we’re done, so we don’t have browsers tarting up the taskbar:

def teardown
  $b.close 
end

“Let’s do something a little more fun. How about… combat training.”

Ok, now that that’s out of the way, let’s open up a browser and go to the site:

setup
go_to_url "http://www.puzzles.setgame.com/puzzle/set.htm"  

You’ll be presented with a playing board like this:

set game

Each of the cards are clickable elements, and will set the checkbox underneath. So next we need to look at the DOM and see if there’s any info in there we can use.

Let’s look at the first card’s DOM contents by doing a right-click followed by Inspect Element in the browser:

set first card dom

Ok, now let’s look at the second one:

set second card dom

And then the third one:

set third card dom

Ok I think we’re starting to see a pattern here–I bet the 12th card in the bottom right corner has the parent element href attribute set to “javascript:board.cardClicked(12)”. Let’s see if that theory holds:

set twelfth card dom

Yep. So that’s good. That means we have something logical to tag onto when trying to figure out what card to click.

It also means we can try a brute-force approach to figuring out what cards make up a set and which don’t.

So let’s make a few more methods to help clean up the code we know we’re going to write.

We know for sure we’ll have to click on a card, so let’s write one that clicks the card based on that href attribute:

def click_card(value)
   $b.element(:xpath => '//a[@href="javascript:board.cardClicked(' + value + ')"]').when_present.click
end

We also know from playing the game manually that when you click 3 cards, you’ll get a popup that either says “GREAT!” for when you get a set, or something else for when we didn’t actually get a set. So let’s put 3 more methods in here to help readability later–one for waiting for the inevitable alert:

def wait_for_alert
  $b.alert.wait_until_present
end

One to check the alert to see if it says “GREAT” indicating you got a set:

def you_found_a_set
  if $b.alert.text.include?("GREAT")
    return true
  else
    return false
  end
end

And then one to close the alert when done, by clicking the OK button:

def close_alert
  $b.alert.when_present.ok
end

Then, at the end of a game, there will be a place for you to enter your username, and also a banner on the next page that says “Congratulations!” so you know your info’s been entered properly. Let’s make those real quick:

def enter_user_id(user)
  # use the select-all-text-and-delete method for entering 
  # the username. 
  $b.text_field(:xpath => "//input[@id='edit-submitted-user-id']").when_present.click
  $b.text_field(:xpath => "//input[@id='edit-submitted-user-id']").when_present.send_keys [:control, 'a']
  $b.text_field(:xpath => "//input[@id='edit-submitted-user-id']").when_present.send_keys [:delete]
  $b.text_field(:xpath => "//input[@id='edit-submitted-user-id']").when_present.send_keys(user)
  $b.text_field(:xpath => "//input[@id='edit-submitted-user-id']").when_present.send_keys [:enter]
end

def wait_for_congratulations
  $b.element(:xpath => "//h1[contains(.,'Congratulations!')]").wait_until_present
end

Awesome. Now for the challenging part.

We’ll need an array to hold the sets that we find. This will come in handy when we want to find out how to stop trying to find sets, and finish up today’s puzzle.

We’ll put this into a global variable, signified by the dollar sign:

$sets = []

Now, as I said earlier, knowing that there’s a number 1 through 12 assigned to each card allows us to brute force the solution.

If you wanted to solve this more elegantly by having your script intelligently find what cards are sets and what aren’t, that’d be a great exercise.

But yeeeeeeeeah, I’m not gonna do that here.

In mathematic terms, what we’re wanting to do is try every possible combination of 3-sets from the values 1 through 12.

It’s a combination as opposed to a permutation, because we can’t pick the same card multiple times. So for example, 1,2,3 is a combination but 1,1,2 is a permutation.

Fortunately, Ruby offers a way to give you all possible combinations for an array of values. So let’s make an array of values from 1 to 12:

a = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]

And then let’s make a loop to go through every possible combination of values:

a.combination(3).each do |combination| 
   *:・゚✧ magic happens here! ✧゚・:*
end

Now we’re gonna make some magic.

For each combination we’ll get 3 values. We’ll click those cards, but we also want to keep track of what cards were clicked, in case it was a set.

Let’s write some code to loop through those 3 values and click the cards represented by them:

a.combination(3).each do |combination| 
  $cards = []
  combination.each do |value|
    click_card(value)
    $cards.push(value)
  end
end

We’re not done yet, now we need to tell the script what to do once a set is clicked. This goes back to the methods we wrote earlier, to tell what to do with that popup that’s going to show up:

a.combination(3).each do |combination| 
  $cards = []
  combination.each do |value|
    click_card(value)
    $cards.push(value)
  end
  wait_for_alert
  if you_found_a_set
    $sets.push($cards)
  end
  close_alert
end

Finally, we add some code at the end to tell this loop when to stop–you don’t want to keep trying to click cards once you’ve found 6 sets:

a.combination(3).each do |combination| 
  $cards = []
  combination.each do |value|
    click_card(value)
    $cards.push(value)
  end
  wait_for_alert
  if you_found_a_set
    $sets.push($cards)
  end
  close_alert
  if $sets.length == 6
    enter_user_id("supahotfire")
    wait_for_congratulations
    break
  end
end

If we run the whole thing, eventually we’ll be able to enter the username for an account that was created before, and then wait for the congratulations banner to signify that the username was entered and processed:

supahotfire
…I spit that.

congratulations
All done! Now to wait for my free card game… Maybe.

Hope You Enjoyed it! Next Challenges…

Was that fun? I think it’s cool that not just software testing can be done, using the same kind of tools.

What are some improvements you can make? How can you make it more efficient? What if you wanted to do it for multiple user accounts?

Hmm… intriguing. Drop a comment and share what you think!

 

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s