------=_Part_1773_7619312.1134419167836
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

Here are my Kalah Players.
My first trick was to create subclass of Player so that my players are
always playing from bins 0..5.  This got rid of a lot of messy
conditions. It also contains a move simulator.

AdamsPlayers.rb contains some very simple test players, as well as 2
reasonably good players:
DeepScoreKalahPlayer tries to find the biggest gain in points for a
single move, and APessimisticKalahPlayer does the same, but subtracts
the opponent's best possible next move.

AHistoryPlayer.rb contains a player which ranks the results of each
move, and keeps a history.  The idea is that the more it plays, the
less likely it is to choose bad moves.
It stores the history in a Yaml file, which definitely causes a
slowdown as it gets bigger.
That's one thing I'd like to improve if I have time.  I also added a
line to the game engine to report the final score back to the players.
 AHistoryPlayer still works without it, but it's less accurate I
think, since it never records the final result.

None of these players pay much attention to the number of stones
remaining on the opponent's side, which is one factor in why they fail
miserably against Dave's player.  But they do tend to beat Rob's
players.  I'm hoping some more people submit solutions.

Add your players to TestMatch.rb to run them in a roundRobin

-Adam

------=_Part_1773_7619312.1134419167836
Content-Type: application/x-ruby; name=AdamsPlayers.rb
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="AdamsPlayers.rb"


#Adapter class - rotates the board so that player's Kalah is always 6
class KalahPlayer < Player
	def choose_move
	  n = (@side==KalahGame::TOP) ? 7 : 0
		@board = @game.board
		@board = @board.rotate n
		return get_move + n
	end
	
	#simulate a move
	def simulate board,i
		b = board.dup
		stones,b[i]=b[i],0
		while stones > 0
			i = 0 if (i+=1) >12
			b[i]+=1
			stones-=1
		end
		if (0..5)===i and b[i]==1
			b[6]+= (b[i]+b[opposite(i)])
			b[i]=b[opposite(i)]=0
		end
		b
	end
	def opposite n
		12-n
	end
	
end

#Some helpers in Array
class Array
	def rotate n
		a =dup
		n.times do a << a.shift end
		a
	end
	def sum
		inject(0){|s,e|s+=e}
	end
	#choose randomly between all items with given value
	def random_index value
		n=rand(find_all{|e|e==value}.size)
		each_with_index{|e,i| return i if e==value and (n-=1)<0 }
	end
end

#### Some simple players for testing:
class RemoveRightKalahPlayer < KalahPlayer
	def get_move
			5.downto(0) {|i| return i if @board[i]>0 }
	end
end
class RemoveHighKalahPlayer < KalahPlayer
	def get_move
			myboard = @board[0,6]
			myboard.index(myboard.max)
	end
end
class RemoveRandomHighKalahPlayer < KalahPlayer
	def get_move
			myboard = @board[0,6]
			myboard.random_index(myboard.max)
	end
end
class RemoveLowKalahPlayer < KalahPlayer
	def get_move
			myboard = @board[0,6].select{|e| e>0}
			@board[0,6].index(myboard.min)
	end
end
class RemoveRandomLowKalahPlayer < KalahPlayer
	def get_move
			myboard = @board[0,6].select{|e| e>0}
			@board[0,6].random_index(myboard.min)
	end
end

class ScoreKalahPlayer < KalahPlayer
	def get_move
		possible_scores = (0..5).map{|i| score_for i}
		possible_scores.index(possible_scores.max)
	end
	def score_for i
	  return -1 if @board[i] == 0
		simulate(@board,i)[6]-@board[6]
	end
end


### Some better players

#Tries to find the biggest increase in score for a turn
class DeepScoreKalahPlayer < KalahPlayer
	def get_move
		best_move(@board)
	end
	def best_move board
		possible_scores = (0..5).map{|i| score_for(board,i)}
		possible_scores.index(possible_scores.max)
	end
	
	#find the increase in score if we make move m
	def score_for board,m
		return -100 if board[m]<1  						  #flag invalid move
	  b, taketurn  = board,true
		while taketurn
			taketurn =  ((b[m]+m)%14 == 6)  #will we land in kalah?
			b = simulate b,m                               
			i = best_move(b) if taketurn             
		end
		b[6]-board[6]                                 #how many points did we gain?
	end
	
end


#Tries to find the biggest increase in score for a turn
#subtracts opponent's possible score
class APessimisticKalahPlayer < DeepScoreKalahPlayer
	MaxDepth = 4
	def get_move
	  @level=0
		best_move(@board)
	end
	def best_move board
		return super(board) if (@level > MaxDepth)
		@level+=1
		possible_scores = (0..5).map{|i| 
			score_for(board,i) - worst_case(simulate(board,i)) 
		}
		@level-=1
		possible_scores.random_index(possible_scores.max)
	end
	#biggest score the opponent can get on this board
	def worst_case board
		worst = 0
		opp_board = board.rotate 7
		6.times {|i|
				s = score_for(opp_board, i)
				worst = s if worst < s
			}
			worst
	end
end




------=_Part_1773_7619312.1134419167836
Content-Type: application/x-ruby; name=AHistoryPlayer.rb
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="AHistoryPlayer.rb"

require 'AdamsPlayers.rb'
require 'yaml'

HistoryFile = "ads_khist.yaml"

class WeightedAverage
	def initialize window
		@store = []
		@size = window
	end
	def add item, weight
		@store.shift if @store.size > @size
		@store << [item,weight]
		self
	end
	def ave
	  totweight = 0
		@store.inject(0){|sum,e| totweight+=e.last; sum+e.first*e.last}/(totweight.to_f)
	end
end

class AHistoryKalahPlayer <APessimisticKalahPlayer
	def initialize name
		super	
		@db = load
		@stack=[]
		@myside = nil
	end
	def reset
	  @stack=[]
		@myside = @side
		save HistoryFile
	end
	
	def get_move
		update_scores @board
	  @level=0
		scores = db_fetch(@board)
		if scores.sum !=0   																#there is data in the record
			scorez = scores.zip((0..5).to_a).sort.reverse 
			m = scorez.first.last
		end
		while m && @board[m]==0
			scorez.shift
			m = scorez.first.last if !scorez.empty?
		end
		m = best_move(@board) if !m                       
	  @stack << [@board,m,WeightedAverage.new(16).add(scores[m],2)]
		save HistoryFile if game_almost_over? simulate(@board,m)
		m
	end
	def scoreboard b
	  b[6]-b[13]
	end
	def update_scores board
		reset if @side!=@myside
		score = scoreboard board
		(1..16).each do |n|
			break if n > @stack.size
			oldboard,move,wave = @stack[-n]
			delta = score-scoreboard(oldboard)  		#did we improve or worsen our relative score?
			db_update(oldboard,move,wave.add(delta,1/Math::sqrt(n)).ave)   #record it, weighted by age
		end
	end
	def game_almost_over? board
		!board[0..5].find{|e| e>0} || board[7..12].find_all{|e| e>0}.size <1
	end
	def key board
		(board[0..5]+board[7..12]).join('-')
	end
	def db_fetch board
		@db[key(board)]||=Array.new(6){0}
	end
	def db_update board,move,score
		  a = db_fetch board
			a[move]=score
		end

	def load
		if File.exists? HistoryFile
			File.open(	HistoryFile,"rb+"){|f| YAML::load(f) } 	
		else
			{}
		end
	end
	def save name
		File.open( name, 'wb' ) {|f| f<< (@db.to_yaml)}
	end

	#I added the following lines to the bottom of KalahGame#play_game so that I could get better scoring. 
	  #>> 	top.notify_over [top_score,bottom_score] if top.respond_to? :notify_over
	  #>> 	bottom.notify_over [bottom_score, top_score] if bottom.respond_to? :notify_over
	#This player still works without these.
	def notify_over score
		final = Array.new(14){0}
		final[6]=score[0]
		final[13]=score[1]
		update_scores final
	end

end



------=_Part_1773_7619312.1134419167836
Content-Type: application/x-ruby; name=KalahMatch.rb
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment; filename="KalahMatch.rb"

class Player
attr_accessor :name
attr_writer :game, :side

def initialize( name )
@name = name
end

def choose_move
if @side==KalahGame::TOP
(7..12).each { |i| return i if @game.stones_at?(i) > 0 }
else
(0..5).each { |i| return i if @game.stones_at?(i) > 0 }
end
end
end

class HumanPlayer < Player
def choose_move
print 'Enter your move choice: '
gets.chomp.to_i
end
end


class KalahMatch
	def start( p1, p2 )
		puts ''
		puts '========== GAME 1 =========='
		p1_score_1, p2_score_1 = KalahGame.new.play_game( p1, p2 )
				
		if p1_score_1 > p2_score_1
			puts p1.name+' won game #1: '+p1_score_1.to_s+'-'+p2_score_1.to_s
		elsif p2_score_1 > p1_score_1
			puts p2.name+' won game #1: '+p2_score_1.to_s+'-'+p1_score_1.to_s
		else
			puts 'game #1 was a tie: '+p1_score_1.to_s+'-'+p2_score_1.to_s
		end
		
		puts ''
		puts '========== GAME 2 =========='		
		p2_score_2, p1_score_2 = KalahGame.new.play_game( p2, p1 )
		
		if p1_score_2 > p2_score_2
			puts p1.name+' won game #2: '+p1_score_2.to_s+'-'+p2_score_2.to_s
		elsif p2_score_2 > p1_score_2
			puts p2.name+' won game #2: '+p2_score_2.to_s+'-'+p1_score_2.to_s
		else
			puts 'game #2 was a tie: '+p1_score_2.to_s+'-'+p2_score_2.to_s
		end
		
		puts ''
		puts '========== FINAL =========='
		
		p1_final = p1_score_1+p1_score_2
		p2_final = p2_score_1+p2_score_2
		
		if p1_final > p2_final
			puts p1.name+' won the match: '+p1_final.to_s+'-'+p2_final.to_s
		elsif p2_final > p1_final
			puts p2.name+' won the match: '+p2_final.to_s+'-'+p1_final.to_s
		else
			puts 'the match was tied overall : '+p1_final.to_s+'-'+p2_final.to_s
		end
	end	
end

class KalahGame
	NOBODY = 0
	TOP = 1
	BOTTOM = 2

  attr_reader :board, :player_to_move

  def initialize_copy(other_game)
      super
      @board = other_game.board.dup
  end
	
	def stones_at?( i )
		@board[i]
	end
	
	def legal_move?( move )
		( ( @player_to_move==TOP and move >= 7 and move <= 12 ) || 
			( @player_to_move==BOTTOM and move >= 0 and move <= 5 ) ) and @board[move] != 0
	end
	
	def game_over?
		top = bottom = true
		(7..12).each { |i| top = false if @board[i] > 0 }
		(0..5).each { |i| bottom = false if @board[i] > 0 }
		top or bottom
	end
	
	def winner
		top, bottom = top_score, bottom_score
		if top > bottom
			return TOP
		elsif bottom > top
			return BOTTOM
		else
			return NOBODY
		end
	end
	
	def top_score
		score = 0
		(7..13).each { |i| score += @board[i] }
		score
	end
	
	def bottom_score
		score = 0
		(0..6).each { |i| score += @board[i] }
		score
	end
	
	def make_move( move )
		( puts 'Illegal move...' ; return ) unless legal_move?( move )
		
		stones, @board[move] = @board[move], 0
		
		pos = move+1
		while stones > 0
			pos+=1 if( (@player_to_move==TOP and pos==6) || (@player_to_move==BOTTOM and pos==13) )
			pos = 0 if pos==14
			@board[pos]+=1
			stones-=1
			pos+=1 if stones > 0
		end
		
		if( @player_to_move==TOP and pos>6 and pos<13 and @board[pos]==1 )
			@board[13] += @board[12-pos]+1
			@board[12-pos] = @board[pos] = 0
		elsif( @player_to_move==BOTTOM and pos>=0 and pos<6 and @board[pos]==1 )
			@board[6] += @board[12-pos]+1
			@board[12-pos] = @board[pos] = 0
		end
		
		if @player_to_move==TOP
			@player_to_move = BOTTOM unless pos == 13
		else
			@player_to_move=TOP unless pos == 6
		end
		
	end
	
	def display
		puts ''
		top = '    '
		[12,11,10,9,8,7].each { |i| top += @board[i].to_s+'  ' }
		puts top
		puts @board[13].to_s + '                     ' + @board[6].to_s
		bottom = '    '
		(0..5).each { |i| bottom += @board[i].to_s+'  ' }
		puts bottom
		puts ''
	end
	
	def reset	
		@board = Array.new( 14, 4 )
		@board[6] = @board[13] = 0	
		@player_to_move = BOTTOM
	end
	
	def play_game( bottom, top )
		reset
		
		bottom.side = BOTTOM
		top.side = TOP
		top.game = bottom.game = self
		
		puts bottom.name+' starts...'
		display
		
		until game_over?
			puts ''
			if @player_to_move == TOP
				move = top.choose_move
				puts top.name+' choose move '+move.to_s
			else
				move = bottom.choose_move
				puts bottom.name+' choose move '+move.to_s
			end
			make_move( move )
			display		
		end
		
		[bottom_score, top_score]		
	end
end

p1 = Player.new( 'Player 1' )
p2 = Player.new( 'Player 2' )
KalahMatch.new.start( p1, p2 )
------=_Part_1773_7619312.1134419167836--