Main

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

August 06, 2006

making geo mashups with ruby and ms virtual earth

I've put together some helper classes for making geo mashups with ruby and MS Virtual Earth.

The first is called 'PlaceFinder', which takes a list of places (stored as a yaml file), then lets that list be searched by name.

In the examples below, I've used my list of NSW suburbs. Unfortunately, the co-ordinates in this file are a kilometer or so out of alignment with the geodetic system used by either the MS or Google map servers, which is noticible when you zoom in to street level, but for a high level 'city wide' view, it's close enough.

Here's an irb session showing how placefinder works:
irb(main):001:0>
irb(main):002:0* require 'placefinder'
=> true
irb(main):003:0> place_finder=PlaceFinder.new
=> #<PlaceFinder:0x2c5d128 @places={}>
irb(main):004:0> place_finder.load_places("nsw_places.yml")
=> true
irb(main):005:0> place_finder.find_by_name("Leura")
=> #<Place:0x310a5e0 @latitude=-33.7089, @longitude=150.335, @postcode=2780, @name="Leura">
irb(main):006:0>

The second module is 'MapMaker', which has a single function 'make_map' that takes a list of points and a zoom factor, and returns html that loads a map from the Microsoft Virtual Earth website, with 'pins' at the specified points

each pin has the following attributes

name
longitude
latitude
caption (which can contain any HTML tags, but should not have any line break characters)

There's some predefined Zoom Factors whose name indicates approximately how large an area will be visible on screen:

MapMaker::ZOOM_STATE=6
MapMaker::ZOOM_GREATER_CITY=9
MapMaker::ZOOM_CITY=10
MapMaker::ZOOM_SUBURB=11

So to put this to use, we need a list of places we want to make pins for. For no particular reason other than that it makes for a fairly simple example, I've decided to plot the home grounds of the 12 rugby clubs that make up the sydney 'grade' competition. (For ancient political reasons, rugby in Sydney is administered in 2 tiers - the top tier is called 'Club Rugby', and the other tier is 'Subbies').

So I went looking for a list of all the sydney club rugby teams which has enough structure to parse with regular expressions.

Screen scraping is a bit of a black art, I'm not going to explain in detail what I'm doing to pull the info out of the web page, but I'm taking advantage of that fact that the data for each team is enclosed in a <tr>..</tr> tag set, and within each  team, the data all has nice regexable prefixes like "Address: ", so a line like this
 
address=team_row.match(/Address: ([^<]*)/)[1]

means "extract the text that comes between the text 'Address: ' and the first '<' character."

The full example follows,  but first, have a look at the output, a map showing the home grounds of the sydney club rugby teams. And all the code needed to make this work (including a list of locations of NSW suburbs) is in the file MapMaker_Demo.zip

require 'map_maker'
require 'placefinder'

place_finder=PlaceFinder.new
place_finder.load_places("nsw_places.yml")

require 'open-uri'

html=open('http://www.tahkids.com.au/aboutthe_TNC.html').read

pin_points=[]

#strip off all the stuff up until the table we're interested in
html.gsub!(/.*CLICK&nbsp;HERE/m,"")

#find all the <tr> elements that contain the text 'Nickname:'
team_rows=html.scan(/<tr>.*?Nickname:.*?<\/tr>/m)

team_rows.each do |team_row|
 #the team name is the first text in bold
 team_name=team_row.match(/<b>(\w[^<]*)/)[1]

 home_ground=team_row.match(/Home Ground: ([^<]*)/)[1]

 address=team_row.match(/Address: ([^<]*)/)[1]

 suburb=address.sub(/.*,\s*/,"") #addresses are all of the form Street, Suburb so strip up to the comma
 
 #some entries have a dud href for the website field so correct them 
 website=team_row.sub(/href="www/,'href="
http://www').match(/http[^"]*/)[0]
 
 caption="<a href=#{website}>#{team_name}</a><br>Home Ground: #{home_ground}"

 #now we've extracted all the interesting data, make up a new pin point to stick on the map
 pin_point=place_finder.find_by_name(suburb)
 pin_point.name=team_name
 pin_point.caption=caption
 pin_points<<pin_point
end

#now make a map with the list of pin points
map_html=MapMaker.make_map(pin_points,MapMaker::ZOOM_CITY)

#save the html
File.open("sydney_club_rugby_map.html","w") {|f| f<<map_html}

July 03, 2006

geocoded NSW postal areas

 A list of suburbs in NSW, including their postcodes and approximate location (using the GDA94 datum).

This was made by merging data from the Geographical Names Board of NSW with Australia Post's Postcode Data File 

June 26, 2006

rendering html in firefox from irb

Here's a Firefox version of yesterday's script to render html from irb.

For best results, go to about:config and set browser.link.open_external to 1 (or from the menu, go to tools->options->tab, and set "open links from other applications in:" to "the most recent tab/window")

here's the script:

require 'base64' 
class Firefox

 #edit to suit
 @@firefox_path='C:\Program Files\Mozilla Firefox\firefox.exe'

 def Firefox.html_as_data_uri(html)
      'data:text/html;base64,'+ Base64.encode64(html).gsub("\n", '')
 end

 def Firefox.show_html(html)
  #can't use fork on windows
  IO.popen("#{@@firefox_path} #{Firefox.html_as_data_uri(html)}")
 end
 
end

June 25, 2006

rendering html from irb

for the last few weeks I've been doing some fairly tedious documentation stuff, e.g. pulling together 'requirements' from out of different word docs into a single list, categorising them (e.g. into 'functional', 'non-functional', 'future') and then creating a new table for each group that lists each requirement with an ID, a description, and a link back to the original source document.

I'm not going to explain why I need to do all that, it's just The Way Things Are Done Around Here.

So I've been using word automation to extract the source docs as html, like this:

def get_html_from_doc(docname)
 	winword = WIN32OLE.new('word.application')
doc=winword.Documents.Open(docname,nil,true)
tempfile=Tempfile.new("tmp")
tempfile.close #word reopens it
doc.SaveAs(tempfile.path,10)
doc.Close
winword.Quit unless winword.nil?
tempfile.open
html=tempfile.read
tempfile.close
html
end
then Rubyful Soup to pull the data from html tables into some linked hashes, which were saved as yaml. Once the data was a collection of ruby objects, I could modify it interactively with irb, then turn it into html. However while irb would happily print out the html source, if I wanted to see how that html would display I needed to first save it to a file, then load that file into IE. That got boring pretty quickly, so I put together this little routine to pipe a html string straight into a MSHTML control.
require 'win32ole'

class ShowHTML

private
def ShowHTML.create_new_window
@@ie=WIN32OLE.new('InternetExplorer.Application')
@@ie.menubar=0
@@ie.toolbar=0
@@ie.statusbar=0
end
ShowHTML.create_new_window
public
def ShowHTML.show(html)
#check our window is still around
begin
@@ie.navigate('about:blank')
rescue WIN32OLERuntimeError
create_new_window
@@ie.navigate('about:blank')
end
@@ie.document.open
@@ie.document.write(html.to_s)
@@ie.document.close
@@ie.visible=true
end

def ShowHTML.show_hash(hash)
html="<html><table>"<<hash.map{|v|"<tr><td>#{v[0]}</td><td>#{v[1]}</td></tr>\n"}.to_s<<"</table>"
ShowHTML.show(html)
end
end

Here's a screenshot of how this looks:
displaying html from irb
irb (foreground) displaying the ENV hash in a html window (background)

March 15, 2006

how IE responds to different HTTP status codes

There is a discussion on intertwingly about feed errors, including the case where a server was serving a valid RSS feed with a 404 (file not found) status code. The feedvalidator was reporting the feed as being non-existent, but IE and firefox would happily display the XML.

I was curious to see how IE handled all the different HTTP status codes, so I put together some ruby scripts to test them.

this is the server: it just listens on port 2000, looks for a 3 digit number in the requested path, and if it finds one returns that number as the HTTP status code in the response, along with a little html page.

require 'webrick'
include WEBrick
s=HTTPServer.new(
 :Port=>2000
)
trap ("INT") {s.shutdown}
s.mount_proc('/') {|req,resp|
     req.path=~/(\d{3})/
     @status=$1?$1:200 
     resp.body="<HTML><body>request:<pre>#{req}</pre>Status:#{@status}<p>#OK#</body></html>"
     resp.status = @status
}
s.start

and this is the client, which uses watir to get IE to call the server, and check the result:

require 'watir'
def test_status(ie,status)
 
 begin
  ie.goto("
http://localhost:2000/#{status}")
  @result=ie.contains_text("#OK")?'OK':'FAIL'
 rescue
  @result='FAIL'
 end
 
 puts "STATUS #{status} -
#{@result}"
end

#list of status codes from http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
#skip 1xx since they causes the client to wait for the server to send a further response
#skip 204 NO CONTENT - watir blocks
#skip 301 / 302  - watir blocks (probably looking for a Location field - according to spec it's not actually mandatory)

ie=Watir::IE.new

%w(
 200 201 202 203 205 206
 300 303 304 305 306 307
 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
 500 501 502 503 504 505
).each {|status| test_status(ie,status)}

ie.close 

Here's the result in IE 6, with 'friendly http errors' disabled ('OK' means that IE rendered the HTML returned, 'FAIL' means IE displayed an error message instead):

STATUS 200 - OK
STATUS 201 - OK
STATUS 202 - OK
STATUS 203 - OK
STATUS 205 - OK
STATUS 206 - OK
STATUS 300 - OK
STATUS 303 - FAIL
STATUS 304 - OK
STATUS 305 - OK
STATUS 306 - OK
STATUS 307 - FAIL
STATUS 400 - OK
STATUS 401 - OK
STATUS 402 - OK
STATUS 403 - OK
STATUS 404 - OK
STATUS 405 - OK
STATUS 406 - OK
STATUS 407 - OK
STATUS 408 - OK
STATUS 409 - OK
STATUS 410 - OK
STATUS 411 - OK
STATUS 412 - OK
STATUS 413 - OK
STATUS 414 - OK
STATUS 415 - OK
STATUS 416 - OK
STATUS 417 - OK
STATUS 500 - OK
STATUS 501 - OK
STATUS 502 - OK
STATUS 503 - OK
STATUS 504 - OK
STATUS 505 - OK

If friendly errors are turned on, this is the result:

STATUS 200 - OK
STATUS 201 - OK
STATUS 202 - OK
STATUS 203 - OK
STATUS 205 - OK
STATUS 206 - OK
STATUS 300 - OK
STATUS 303 - FAIL
STATUS 304 - OK
STATUS 305 - OK
STATUS 306 - OK
STATUS 307 - FAIL
STATUS 400 - FAIL
STATUS 401 - OK
STATUS 402 - OK
STATUS 403 - OK
STATUS 404 - FAIL
STATUS 405 - OK
STATUS 406 - FAIL
STATUS 407 - OK
STATUS 408 - FAIL
STATUS 409 - FAIL
STATUS 410 - OK
STATUS 411 - OK
STATUS 412 - OK
STATUS 413 - OK
STATUS 414 - OK
STATUS 415 - OK
STATUS 416 - OK
STATUS 417 - OK
STATUS 500 - FAIL
STATUS 501 - FAIL
STATUS 502 - OK
STATUS 503 - OK
STATUS 504 - OK
STATUS 505 - FAIL

 

February 23, 2006

Example of connecting to SQL Server via Ruby

I'm porting my MSSQL Schema Explorer app to Ruby

In the process, I discovered there's not much documentation around on how to use the ADO provider in Ruby DBI to connect to a SQL Server. here's a little example that lists the names of all the databases on the server 'localhost'. It wil work on SQL Server 2005 - if you are using SQL Server 2000 you will need to change the text 'Provider=SQLNCLI' to 'Provider=SQLOLEDB'

####

require 'dbi'

class Server
 attr_reader :name
 def initialize(name)
  @server_name=name
  @dbh=DBI.connect("DBI:ADO:Provider=SQLNCLI;Data Source=#{name};Integrated Security=SSPI")
 end
 
 def databases
  db=Array.new
  @dbh.select_all('SELECT name FROM master.sys.databases ORDER BY 1') do | row |
   db.<< Database.new(@dbh,row[0])
  end  
  db
 end
end

class Database
 attr_reader :name
 def initialize(dbh,name)
  @dbh=dbh
  @name=name
 end
end


server=Server.new("localhost")
server.databases.each {|x| p x.name}