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