Here's my submission.  I take no credit for the basic idea, which is
shamelessly ripped from Dan Egnor's famous "Iocaine Powder" program;
mostly what I was trying to do was provide a clean Ruby implementation
of Dan's insights.

For reasons that will become clear later on, this submission actually
contains several subclasses of Player in the same file - if this is a
problem when running the competition, let me know, it's easy to work
around.  However, the "real" player being submitted here is
AJBMetaPlayer.

The basic idea behind AJBMetaPlayer is this: it keeps an array of
other instances of Player subclasses.  When it gets handed the
result(), it passes the information along to each of these players;
when it needs to choose(), it picks just one of the players and
returns its choice.  It also keeps a running tally on each move of
which of the player instances would have won or lost if it had been
the one chosen for that move, and then uses that when picking a player
for next time.  It will always pick the player that would have had the
best record so far, had it been chosen every time.

The interesting twist is that AJBMetaPlayer also makes use of two
PlayerDecorators, which present a Player interface but are wrapped
around existing players.  One of these is the Inverter, which reverses
the results that are provided to it: a Player instance wrapped in an
Inverter will see the world as your opponent sees it.  The other is
the Defeater, which shifts the choices made in choose() so that they
defeat the original choice (ie, :rock becomes :paper and so on).  By
using all of the possible combinations of these, a single Player class
can show up 6 times in the AJBMetaPlayer's players array: normally,
defeated, defeated twice (so :rock becomes :scissors), inverted,
inverted + defeated, inverted + defeated + defeated.  This allows it
to model all of the potential second- and triple-guessing an opponent
might be doing.  The generic algorithm for picking the best player
instance will automatically detect and exploit this if it's there.

Absent any randomness, if you have a player instance identical to your
opponent in the players array, wrapped with an Inverter (so it gets
the same results your opponent does) and then a Defeater, you will
beat it every time.  Indeed, if it were considered legal, a very
effective approach would be to search for all active Player subclasses
and construct an instance of each, wrapped in all the possible
variations.  Since this seems a little too much like cheating,
AJBMetaPlayer by default uses only two base strategies that are
hopefully representative of the deterministic players it will
encounter.  One of these, AJBFrequencyPlayer, just counters whatever
move its opponent has played most often in the past.  The other,
AJBHistoryPlayer, builds a tree of its opponents previous runs of
moves (of lengths 1..20), and assumes that if it finds the same run
again, the player might continue it in the same way.  The meta player
should do quite well against players that are either susceptible to
such analysis, *or are using a sufficiently similar analysis
themselves*.  For safety, there's also AJBRandomPlayer thrown in by
default, as a fallback if nothing else seems to work.

Here's the code:
# AJBMetaPlayer.rb
# (c) Avi Bryant 2005
# heavily inspired by Dan Egnor's "Iocaine Powder":
# http://dan.egnor.name/iocaine.html

class Hash
    def max_key
        max{|a,b| a[1]<=>b[1]}[0] unless empty?
    end
end

class Symbol
    def defeat
        case self
        when :paper
            :scissors
        when :scissors
            :rock
        when :rock
            :paper
        end
    end
end

class AJBRandomPlayer < Player
    def choose
         [:paper, :scissors, :rock][rand(3)]
    end
end
   
class AJBFrequencyPlayer < AJBRandomPlayer
    def initialize(op)
        super
        @frequencies = Hash.new(0)
    end
    
    def choose
        (@frequencies.max_key || super).defeat
    end
    
    def result(y, t, win)
        @frequencies[t] += 1
    end
end

class AJBHistoryPlayer < AJBRandomPlayer

    class Node        
        def initialize
            @children = {}
            @scores = Hash.new(0)
        end
    
        def add_child(key)
            @scores[key] += 1
            @children[key] ||= Node.new
        end
        
        def add_scores_to(totals)
            @scores.each{|k,v| totals[k] += v}
        end
    end

    MaxNodes = 20
    
    def initialize(op)
        super
        @nodes = []
        @root = Node.new
    end
    
    def choose
        scores = Hash.new(0)
        @nodes.each{|n| n.add_scores_to(scores)}
        (scores.max_key || super).defeat
    end
    
    def result(y, t, win)
        (@nodes << @root).collect!{|n| n.add_child(t)}
        @nodes.shift until @nodes.size <= MaxNodes
   end
end

class AJBMetaPlayer < Player
    class PlayerDecorator
        def initialize(player)
            @player = player
        end
    end
    
    class Defeater < PlayerDecorator
        def choose
            @player.choose.defeat
        end
        
        def result(y, t, win)
        end
    end
    
    class Inverter < PlayerDecorator
        def choose
            @player.choose
        end
        
        def result(y, t, win)
            @player.result(t, y, !win)
        end
    end
    
    def initialize(op)
        super
        @players =  [AJBRandomPlayer.new(op)] +
                    variations(AJBHistoryPlayer) +
                    variations(AJBFrequencyPlayer)
        @scores = {}
        @players.each{|p| @scores[p] = 0}
    end
        
    def result(y, t, win)
        @players.each{|p| score(p, t)}
        @players.each{|p| p.result(y, t, win)}
    end
    
    def choose
        @scores.max_key.choose
    end
    
    :private
    
    def variations(klass)
        straight = klass.new(@opponent)
        inverted = Inverter.new(klass.new(@opponent))
        [straight,
        inverted,
        Defeater.new(straight),
        Defeater.new(inverted),
        Defeater.new(Defeater.new(straight)),
        Defeater.new(Defeater.new(inverted))]
    end

    def score(player, move)
        @scores[player] += ScoreTable[[player.choose, move]]
    end 
    
    ScoreTable =
        {[:scissors, :rock] => -1,
        [:scissors, :scissors] => 0,
        [:scissors, :paper] => 1,
        [:paper, :rock] => 1,
        [:paper, :scissors] => -1,
        [:paper, :paper] => 0,
        [:rock, :rock] => 0,
        [:rock, :scissors] => 1,
        [:rock, :paper] => -1}   
end