« June 2007 | Main | August 2007 »

July 29, 2007

writing rock-paper-scissors bots in ruby

RubyShamBo is a framework for hosting RoShamBo (also known as Rock-Paper-Scissors) tournaments between different computer players (aka bots). RoShamBo is a simple game played between two players. On each turn, the players simultaneously choose one of "rock", "paper", or "scissors". If they choose the same item, the result is a tie; otherwise paper covers rock, scissors cuts paper, or rock crushes scissors. A match consists of a series of turns between the two players.

The game is trivial from a game-theory point of view. The optimal mixed strategy is to choose an action uniformly at random (one-third probability of each). This will ensure a break-even result in the long run, regardless of how strong (or how weak!) the opponent is.

However, against predictable opponents, a player can attempt to detect patterns in the opponent's play, and exploit those weaknesses with an appropriate counter-strategy.

A RubyShamBo tournament pits ruby programs, each implimenting different strategies, against each other. RubyShamBo was inspired by the International RoShamBo Programmming Competition. Much of this summary has been shamelessly stolen from the original announcement of that competition. After each turn, the winning bot recieves 1 point, and the losing bot losses 1 point. If the game ends in a tie, no points are awarded. At the end of the tournament, the bots are ranked by the total number of points they have gained in all matches against all bots.

Some of the programs in a RubyShamBo tournament will use sub-optimal strategies, and will be vulnerable to a perceptive and adaptive opponent. Of course, there is always a risk associated with such a prediction, as that player may be attempting to trap its opponent by anticipating the reaction to previous plays.

The most successful programs will recognize a variety of patterns and relationships, and use that information to gain an advantage over each opponent, without being susceptible to similar attacks.

At this stage there are no firm plans to host a public RubyShamBo tournament, but if you'd like to enter (or even help organise) such an event, please send an email to jonno at jamtronix dot com expressing your interest.

running a tournament

First, you will need to have ruby installed. Windows users should get the latest version of the One-Click Ruby Installer. Users of other platforms can consult the Ruby downloads site for a suitable version.

Once ruby is set up on your machine, download the zip file containing latest RubyShamBo release and unzip to somewhere on to your local drive. The zip file will include the following folders :

  • lib/ which contains 'rubyshambo.rb' which has all of the code for hosting a tournament
  • bots/ which has a number of files each containing the code for a single bot
  • test/ which contains 'test_tournament.rb', which will set up and run a tournament that pits every bot defined in the bots/ directory against each other, and then print out the results.
At the command line, cd to the test/ directory and type ruby test_tournament.rb. After about a second, the tournament results will be printed to the screen like so:
C:\src\rubyshambo\test>ruby test_tournament.rb
Wagner 1            : 6027
YoungGun 1          : 2925
CivilWarBuff 1      : 2596
Bot 1               : 1
OldGeneral 1        : -442
SensitiveLefty 1    : -586
SportsFan 1         : -2608
BrokenRecord 1      : -7913
This shows the most successful bot in this tournament is 'Wagner 1' which won 6027 more turns than it lost. The least succesful bot was 'BrokenRecord 1' which lost 7913 more turns than it won.

creating your own bot

Creating a new RubyShamBo bot requires some knowledge of programming in ruby. You should at least be able to complete the introductory browser-based tutorial at the Try Ruby! website. The rules that must be followed by each bot competing in a RubyShamBo tournament are:
  1. The bot must be a subclass of Bot (which is defined in rubyshambo.rb)
  2. The bot must impliment a method called 'get_throw' which takes a single parameter, which is a MatchHistory object (also defined in rubyshambo.rb), and returns a symbol, which must be either :rock, :paper, or :scissors.
  3. If you define an 'initialize' method for the bot, it must not have any mandatory parameters (else the tournament framework can't autoload it)

The MatchHistory object that gets passed in to each call contains the following fields:

  • turns_played will be 0 on the first turn against a new opponent
  • my_throws is an array recording every throw you've made in the current match. Since ruby arrays are zero-indexed, match_history.my_throws[0] will have the throw you selected on the first turn in this match, match_history.my_throws[6] will have the move you made on the seventh turn, and match_history.my_throws.last will have the last throw you made.
  • opponent_throws is an array recording every throw the current opponent has made in this match. The throw the opponent made last turn is in match_history.opponent_throws.last
  • my_score is the score for the current match. The opponents score for the current match will always be the inverse of your score. i.e. if match_history.my_score is 5, that means you have won 5 more turns than you have lost in the current match (and thus the opponent's score must be -5).
There are some helper methods and arrays you can use:
  • VALID_THROWS is an array containing [:paper,:scissors,:rock], i.e. VALID_THROWS[1] will return :scissors
  • WINNING_THROW is a hash where the keys are possible throws and the values are the throw that beats the key. i.e. WINNING_THROW[:rock] returns :paper.
  • Turn.random_throw will return a random throw.

an example bot

Here is the bot called 'Old General' (found in bots/oldgeneral.rb). He's called that because he's always planning to fight the last war. On the first turn he throws at random. On every subsequent turn, he throws whatever would have won the last turn
class OldGeneral<Bot
	def get_throw(match_history)
		if (match_history.turns_played==0) then
			return Turn.random_throw
		end
		return WINNING_THROW[match_history.opponent_throws.last]
	end	
end

useful links