James Edward Gray II’s Ruby Quiz #16 was to implement Rock, Paper, Scissors playing classes to compete on a playing field managed by a given Game class. Today we revisit this quiz for a bit of coding fun. We’ll implement some simple players and move on to some basic metaprogramming techniques and write players that manipulate each other or the game itself.
If you want to follow along:
1 2 3 4 5 6 7 8 9 | |
Simple Player
Right. So here’s what a very simple player class looks like.
1 2 3 4 5 6 | |
The rock_paper_scissors.rb defines a Player class, a Game class, and
provides a script to run the contest. Our own custom player classes just have to
inherit from the given Player class and implement a choose method that returns
:rock, :paper, or :scissors. Our players can optionally implement a
result method that is used as a callback from the game to allow our players to
see the result of their play. If our players have an initialize method it is
called with the classname of the opponent.
So, back to Michael Bluth (or Poor Predictable Bart) who always chooses rock.
His choose method just returns the symbol :rock. Easy. Let’s write a player
to beat him. Also easy.
1 2 3 4 5 6 | |
GOB will beat Michael Bluth 100% of the time.
1 2 3 4 5 | |
Ok, now let’s write a player to beat them both. Consistently.
Reactive Player
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
This player is kind of interesting, but still straightforward. Reactive has a
constant MOVE_THAT_BEATS that maps moves to the move that would beat them.
Reactive then uses that knowledge to play the move that beats the last move
played by his opponent (or :paper). This strategy should prove extremely
effective against a player who always makes the same move.
1 2 3 4 5 6 7 8 9 | |
Yep, Always Rock loses every game because Reactive plays paper from the start. Paper gets in one draw, then loses every game afterwards.
Now, let’s write a player that will beat all of the players thus far.
Random Player
Theoretically, the best strategy in Rock, Paper, Scissors is to be completely random. That’s a pretty easy strategy for a computer to play (although surprisingly difficult for a human) so let’s code that up next.
1 2 3 4 5 | |
Well that was easy. Let’s pit Random against some of the players so far.
1 2 | |
1 2 | |
Fun, but not all that interesting. How about we write a player who watches their opponent and builds a strategy to defeat them? This player is going to be a bit more complex than those we’ve written so far.
Pattern Matching Player
The plan: build up a pattern memory of player moves, opponent moves, and the next move the opponent played under those conditions. After every move:
- remember the moves that were just played
- record the previous game’s moves and the opponents move
- use that record of game patterns to determine the opponent’s next likely move and play the move that beats it
This pattern set will allow us to make observations such as “the last ten times I played rock and my opponent played paper, my opponent’s next move was scissors” or “out the last 97 times I played rock and my opponent played scissors my opponent played scissors next 96% of the time”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
This guy looks complicated, but the only tricky logic is around the whole “Is
this the first game or not?” I’m not entirely happy with the way I’ve worked it
out here, but it gets the job done. store_pattern and their_likely_next_move
are the two key methods. The move patterns are stored as nested hashes [my
move][opponent move] => [array of observed moves] e.g. `[:rock][:paper] =>
[:scissors, :paper, :scissors, :scissors, :scissors, :paper]
their_likely_next_move just looks into the pattern and counts up the data seen
so far. If there’s no data yet, it just guesses randomly.
Let’s see how our pattern matcher fares!
1 2 3 4 5 | |
Not bad! Our pattern matcher correctly determines the simple pattern employed by Always Rock
1 2 3 4 5 | |
Bam! Our pattern matcher handily figures out what our simple Reactive player is likely to do. Now for a real challenge.
1 2 3 4 5 6 7 8 9 10 11 12 | |
So PatternMatching fares well against RandomPlayer but can’t consistently win. Let’s make a player who can consistently and completely defeat RandomPlayer. Let’s make a player who cheats.
Cheater
1 2 3 4 5 6 7 8 9 | |
Mwahaha! Cheater utilizes that so far unused ability to get the name of his opponent. Using that and some magic Cheater hypnotizes his opponent into always playing scissors.
Does it work? Absolutely.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
So what’s going on here? To answer that, let’s dive into irb
1 2 3 4 5 | |
Ok, we’re in irb and we’ve got our game and a player loaded. Let’s see what Cheater is up to.
1
| |
1 2 3 4 | |
const_get checks a module for a constant with the given name. Since a class is
defined as a constant and Kernel sits over every class, we can ask Kernel to
find our opponent’s class for us. Which is what we’re doing here.
Basically Kernel.const_get('AlwaysRock') is an easy way to turn the string
“AlwaysRock” into a constant.
Now class_eval. This method just says to a class, “evaluate this string as if
it were written as part of your code.”
1 2 3 4 | |
Put those pieces together, and our Cheater:
- Takes the string of his opponents name and turns it into a constant to get at his opponent’s class.
- Rewrites his opponent with a new
choosemethod that he controls. - Beats his opponents modified choose method.
1 2 3 4 5 6 | |
Insidious! Even worse, if you’re running a contest with more than two players the cheater permanently modifies his opponents into always playing their redefined choose method.
Let’s level the playing field and force the cheater to play fair.
Level Playing Field
LevelPlayingField avoids being manipulated by the cheater by having a strong will to resist Cheater’s hypnotism. Cheater works by modifying the class of his opponents, so LevelPlayingField doesn’t have a choose method defined by his class. LevelPlayingField defines his own choose method on initialization.
1 2 3 4 5 6 7 8 9 10 | |
Yes, that’s right. LevelPlayingField uses the same trick that Cheater does, but on himself to try and guarantee he’s playing his own strategy.
1 2 3 4 5 6 7 8 | |
1 2 3 4 5 | |
Now this works, but there’s one flaw. Our LevelPlayingField player only succeeds when he’s initialized after the Cheater.
1 2 3 4 5 | |
If Cheater comes into play after LevelPlayingField has completed his trick to
try and isolate his choose method, then the Cheater will still just override
LevelPlayingField’s choose method. It’s a remarkably tough strategy to defeat.
Any trick we use to cleverly hide our choose method from the Cheater can
ultimately be sidestepped by the Cheater just supplying a new choose method.
Even if we write a player who removes the ability for the Cheater to cheat, if he’s loaded first: he wins.
1 2 3 4 5 6 7 8 9 | |
So let’s bring in a player who always wins. A player who out-cheats the cheater. A player who modifies the game itself.
Batman
1 2 3 4 5 6 7 8 9 10 11 12 | |
Batman’s playing strategy is to enter the Game class itself and ensure that he’s the winner no matter what’s been played. Not only that, but he completely alters the game for all other player matches. They all get recorded as draws, since only Batman can win.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Gosh, makes you almost feel bad for his opponents.
I hope you’ve enjoyed this bit of fun. On Thursday I plan to take this small glimpse into the world of metaprogramming a bit further.