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 inthis 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
$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 "https://www.puzzles.setgame.com/puzzle/set.htm"
You’ll be presented with a playing board like this:
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:
Ok, now let’s look at the second one:
And then the third one:
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:
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 => "https://input[@id='edit-submitted-user-id']").when_present.click
$b.text_field(:xpath => "https://input[@id='edit-submitted-user-id']").when_present.send_keys [:control, 'a']
$b.text_field(:xpath => "https://input[@id='edit-submitted-user-id']").when_present.send_keys [:delete]
$b.text_field(:xpath => "https://input[@id='edit-submitted-user-id']").when_present.send_keys(user)
$b.text_field(:xpath => "https://input[@id='edit-submitted-user-id']").when_present.send_keys [:enter]
end
def wait_for_congratulations
$b.element(:xpath => "https://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:
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!
Podcasting with a Mission
9 年Well that's terrible. The formatting's all gone. Here's a link to the original until I can fix this post: https://testzius.wordpress.com/2015/12/05/automating-the-online-set-card-game/ sry :(