OSDN Git Service

[shogi-server] shogi_server/pairing.rb: Attempt more trials
[shogi-server/shogi-server.git] / shogi_server / pairing.rb
index bb840fe..cc2ea62 100644 (file)
@@ -1,7 +1,7 @@
 ## $Id$
 
 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
-## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
+## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
 ##
 ## This program is free software; you can redistribute it and/or modify
 ## it under the terms of the GNU General Public License as published by
@@ -24,51 +24,67 @@ module ShogiServer
   class Pairing
 
     class << self
-      def default_factory
-        return least_diff_pairing
+      def default_factory(options)
+        return least_diff_pairing(options)
       end
 
-      def sort_by_rate_with_randomness
+      def sort_by_rate_with_randomness(options)
         return [LogPlayers.new,
-                ExcludeSacrificeGps500.new,
+                ExcludeSacrifice.new(options[:sacrifice]),
                 MakeEven.new,
                 SortByRateWithRandomness.new(1200, 2400),
                 StartGameWithoutHumans.new]
       end
 
-      def random_pairing
+      def random_pairing(options)
         return [LogPlayers.new,
-                ExcludeSacrificeGps500.new,
+                ExcludeSacrifice.new(options[:sacrifice]),
                 MakeEven.new,
                 Randomize.new,
                 StartGameWithoutHumans.new]
       end
 
-      def swiss_pairing
+      def swiss_pairing(options)
         return [LogPlayers.new,
-                ExcludeSacrificeGps500.new,
+                ExcludeSacrifice.new(options[:sacrifice]),
                 MakeEven.new,
                 Swiss.new,
                 StartGameWithoutHumans.new]
       end
 
-      def least_diff_pairing
+      def least_diff_pairing(options)
         return [LogPlayers.new,
-                ExcludeSacrificeGps500.new,
+                ExcludeSacrifice.new(options[:sacrifice]),
                 MakeEven.new,
                 LeastDiff.new,
                 StartGameWithoutHumans.new]
       end
 
-      def match(players)
-        logics = default_factory
+      def floodgate_zyunisen(options)
+        return [LogPlayers.new,
+                ExcludeUnratedPlayers.new,
+                ExcludeSacrifice.new(options[:sacrifice]),
+                MakeEven.new,
+                LeastDiff.new,
+                StartGameWithoutHumans.new]
+      end
+
+      def match(players, logics, options)
         logics.inject(players) do |result, item|
+          item.set_options(options)
           item.match(result)
           result
         end
       end
     end # class << self
 
+    def initialize
+      @options = {}
+    end
+
+    def set_options(options)
+      @options.merge!(options)
+    end
 
     # Make matches among players.
     # @param players an array of players, which should be updated destructively
@@ -121,7 +137,7 @@ module ShogiServer
       log_message("Floodgate: Starting a game: BLACK %s vs WHITE %s" % [p1.name, p2.name])
       p1.sente = true
       p2.sente = false
-      board = Board.new
+      board = Board.new(@options)
       board.initial
       Game.new(p1.game_name, p1, p2, board)
     end
@@ -136,7 +152,7 @@ module ShogiServer
     def match(players)
       super
       if players.size < 2
-        log_warning("Floodgate: There should be more than one player (%d)." % [players.size])
+        log_message("Floodgate: There are less than two players: %d" % [players.size])
         return
       end
       if players.size.odd?
@@ -159,7 +175,7 @@ module ShogiServer
       super
       log_players(players)
       if players.size < 2
-        log_warning("Floodgate: There should be more than one player (%d)." % [players.size])
+        log_message("Floodgate: There are less than two players: %d" % [players.size])
         return
       elsif players.size == 2
         start_game_shuffle(players)
@@ -298,7 +314,7 @@ module ShogiServer
     def match(players)
       super
       return if less_than_one?(players)
-      one = players.choice
+      one = players.sample
       log_message("Floodgate: Deleted %s at random" % [one.name])
       players.delete(one)
       log_players(players)
@@ -346,7 +362,7 @@ module ShogiServer
     # @sacrifice a player id to be eliminated
     def initialize(sacrifice)
       super()
-      @sacrifice = sacrifice
+      @sacrifice = sacrifice || "gps500+e293220e3f8a3e59f79f6b0efffaa931"
     end
 
     def match(players)
@@ -388,54 +404,74 @@ module ShogiServer
       players.shuffle
     end
 
-    def average_rate(players)
-      n=0
-      sum=0
-      players.find_all{|p| p.rate}.each do |p|
-        n += 1
-        sum += p.rate
+    # Update estimated rate of a player.
+    # 1. If it has a valid rate, return the rate.
+    # 2. If it has no valid rate, return:
+    #   a. If it won the last game, the opponent's rate + 200
+    #   b. If it lost the last game, the opponent's rate - 200
+    #   c. otherwise, return 2150 (default value)
+    #
+    def estimate_rate(player, history)
+      player.estimated_rate = 2150 # default value
+
+      unless history
+        log_message("Floodgate: Without game history, estimated %s's rate: %d" % [player.name, player.estimated_rate])
+        return
+      end
+
+      g = history.last_valid_game(player.player_id)
+      unless g
+        log_message("Floodgate: Without any valid games in history, estimated %s's rate: %d" % [player.name, player.estimated_rate])
+        return
+      end
+
+      opponent_id = nil
+      win         = true
+      case player.player_id
+      when g[:winner]
+        opponent_id = g[:loser]
+        win = true
+      when g[:loser]
+        opponent_id = g[:winner]
+        win = false
+      else
+        log_warning("Floodgate: The last valid game is invalid for %s!" % [player.name])
+        log_message("Floodgate: Estimated %s's rate: %d" % [player.name, player.estimated_rate])
+        return
+      end
+
+      opponent_name = opponent_id.split("+")[0]
+      p = $league.find(opponent_name)
+      unless p
+        log_message("Floodgate: No active opponent found. Estimated %s's rate: %d" % [player.name, player.estimated_rate])
+        return
+      end
+
+      opponent_rate = 0
+      if p.rate != 0
+        opponent_rate = p.rate
+      elsif p.estimated_rate != 0
+        opponent_rate = p.estimated_rate
       end
 
-      return n > 0 ? sum/n : 2150 # interger
+      if opponent_rate != 0
+        player.estimated_rate = opponent_rate + (win ? 200 : -200)
+      end
+
+      log_message("Floodgate: Estimated %s's rate: %d" % [player.name, player.estimated_rate])
     end
 
-    # Returns a player's rate value.
-    # 1. If it has a valid rate, return the rate.
-    # 2. If it has no valid rate, return average of the following values:
-    #   a. For games it won, the opponent's rate + 100
-    #   b. For games it lost, the opponent's rate - 100
-    #   (if the opponent has no valid rate, count out the game)
-    #   (if there are not such games, return 2150 (default value)
+    # Return a player's rate based on its actual rate or estimated rate.
     #
     def get_player_rate(player, history)
-      return player.rate if player.rate != 0
-      return 2150 unless history
-
-      count = 0
-      sum = 0
-
-      history.win_games(player.player_id).each do |g|
-        next unless g[:loser]
-        name = g[:loser].split("+")[0]
-        p = $league.find(name)
-        if p && p.rate != 0
-          count += 1
-          sum += p.rate + 100
-        end
-      end
-      history.loss_games(player.player_id).each do |g|
-        next unless g[:winner]
-        name = g[:winner].split("+")[0]
-        p = $league.find(name)
-        if p && p.rate != 0
-          count += 1
-          sum += p.rate - 100
-        end
+      if player.rate != 0
+        return player.rate
+      elsif player.estimated_rate != 0
+        return player.estimated_rate 
+      else
+        estimate_rate(player, history)
+        return player.estimated_rate
       end
-
-      estimate = (count == 0 ? 2150 : sum/count)
-      log_message("Floodgate: Estimated rate of %s is %d" % [player.name, estimate])
-      return estimate
     end
 
     def calculate_diff_with_penalty(players, history)
@@ -466,11 +502,34 @@ module ShogiServer
         if p1.is_human? && p2.is_human?
           ret += 800
         end
+
+        # 2.3 a match with likely kin players
+        if (p1.player_id[0..6] == p2.player_id[0..6])
+          ret += 800
+        elsif (p1.player_id[0..3] == p2.player_id[0..3])
+          ret += 400
+        end
       end
 
       ret
     end
 
+    # Total combinations of possible games among n players
+    #   nC2 * (n-2)C2 * ... * 2C2 / (n/2)!
+    def total_posibilities(n)
+      n -= 1 if n.odd?
+      return 1 if n <= 2
+
+      ret = 1
+      i = n
+      while i >= 2 do
+        ret *= ::ShogiServer::nCk(i,2)
+        i -= 2
+      end
+      ret /= ::ShogiServer::factorial(n/2)
+      return ret
+    end
+
     def match(players)
       super
       if players.size < 3
@@ -478,12 +537,19 @@ module ShogiServer
         return players
       end
 
-      # 10 trials
+      # Reset estimated rate
+      players.each {|p| p.estimated_rate = 0}
+
       matches = []
       scores  = []
       path = ShogiServer::League::Floodgate.history_file_path(players.first.game_name)
       history = ShogiServer::League::Floodgate::History.factory(path)
-      10.times do 
+
+      # Increase trials, depending on a number of players
+      trials = [300, total_posibilities(players.size)/3].min
+      trials = [10, trials].max
+      log_message("Floodgate: %d trials" % [trials])
+      trials.times do
         m = random_match(players)
         matches << m
         scores << calculate_diff_with_penalty(m, history)
@@ -504,10 +570,23 @@ module ShogiServer
           min_score = s
         end
       end
-      log_message("Floodgate: the least score %d (%d per player)" % [min_score, min_score/players.size])
+      log_message("Floodgate: the least score %d (%d per game) [%s]" % [min_score, min_score/players.size*2, scores.join(" ")])
 
-      players = matches[min_index]
+      players.replace(matches[min_index])
     end
   end
 
+  # This pairing method excludes unrated players
+  #
+  class ExcludeUnratedPlayers < Pairing
+
+    def match(players)
+      super
+
+      log_message("Floodgate: Deleting unrated players...")
+      players.delete_if{|a| a.rate == 0}
+      log_players(players)
+    end
+  end # class ExcludeUnratedPlayers
+
 end # ShogiServer