OSDN Git Service

Fix a bug.
[shogi-server/shogi-server.git] / shogi-server
index 915ab14..c4b8673 100755 (executable)
@@ -38,6 +38,9 @@ require 'getoptlong'
 require 'thread'
 require 'timeout'
 require 'socket'
+require 'yaml'
+require 'yaml/store'
+require 'digest/md5'
 
 TCPSocket.do_not_reverse_lookup = true
 Thread.abort_on_exception = true
@@ -89,15 +92,19 @@ class League
     @games = Hash::new
     @players = Hash::new
     @event = nil
+    @db = YAML::Store.new( File.join(File.dirname(__FILE__), "players.yaml") )
   end
   attr_accessor :players, :games, :event
 
   def add(player)
+    self.load(player) if player.id
     @players[player.name] = player
   end
+  
   def delete(player)
     @players.delete(player.name)
   end
+  
   def get_player(status, game_name, sente, searcher=nil)
     @players.each do |name, player|
       if ((player.status == status) &&
@@ -109,12 +116,38 @@ class League
     end
     return nil
   end
+  
+  def save
+    @db.transaction do
+      @players.each_value do |p|
+        next unless p.id
+        @db[p.id] = {'name' => p.name, 'rate' => p.rate}
+      end
+    end
+  end
+
+  def load(player)
+    hash = search(player.id)
+    if hash
+      # a current user
+      player.rate = hash['rate']
+    end
+  end
+
+  def search(id)
+    hash = nil
+    @db.transaction do
+      hash = @db[id]
+    end
+    hash
+  end
 end
 
 class Player
   def initialize(str, socket)
     @name = nil
     @password = nil
+    @id = nil, @rate = nil      # used by rating
     @socket = socket
     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
 
@@ -130,9 +163,11 @@ class Player
     login(str)
   end
 
-  attr_accessor :name, :password, :socket, :status
+  attr_accessor :name, :password, :socket, :status, :rate
   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
   attr_accessor :main_thread, :writer_thread, :write_queue
+  attr_reader   :id
+  
   def kill
     log_message(sprintf("user %s killed", @name))
     if (@game)
@@ -191,6 +226,8 @@ class Player
     @eol = $1
     str.chomp!
     (login, @name, @password, ext) = str.split
+    @name, trip = @name.split(",") # used by rating
+    @id = trip ? Digest::MD5.hexdigest("#{@name},#{@trip}") : nil
     if (ext)
       @protocol = "x1"
     else
@@ -201,7 +238,7 @@ class Player
       writer()
     end
   end
-    
+  
   def run
     write_safe(sprintf("LOGIN:%s OK\n", @name))
     if (@protocol != "CSA")
@@ -791,8 +828,10 @@ class Board
     @sente_history = Hash::new
     @gote_history = Hash::new
     @array = [[], [], [], [], [], [], [], [], [], []]
+    @move_count = 0
   end
   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
+  attr_reader :move_count
 
   def initial
     PieceKY::new(self, 1, 1, false)
@@ -873,6 +912,7 @@ class Board
       end
       @array[x0][y0].move_to(x1, y1)
     end
+    @move_count += 1
     return true
   end
 
@@ -999,11 +1039,21 @@ class Board
   end
 
   def good_kachi?(sente)
-    return false if (checkmated?(sente))
+    if (checkmated?(sente))
+      puts "'NG: Checkmating." if $DEBUG
+      return false 
+    end
+    
     ou = look_for_ou(sente)
-    return false if (sente && (ou.y >= 4))
-    return false if (! sente && (ou.y <= 6))
-
+    if (sente && (ou.y >= 4))
+      puts "'NG: Black's OU does not enter yet." if $DEBUG
+      return false     
+    end  
+    if (! sente && (ou.y <= 6))
+      puts "'NG: White's OU does not enter yet." if $DEBUG
+      return false 
+    end
+      
     number = 0
     point = 0
 
@@ -1030,16 +1080,27 @@ class Board
       point = point + piece.point
     end
 
-    return false if (number < 10)
+    if (number < 10)
+      puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
+      return false     
+    end  
     if (sente)
-      return false if (point < 28)
+      if (point < 28)
+        puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
+        return false 
+      end  
     else
-      return false if (point < 27)
+      if (point < 27)
+        puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
+        return false 
+      end
     end
+
+    puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
     return true
   end
 
-  def handle_one_move(str)
+  def handle_one_move(str, sente)
     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
       sg = $1
       x0 = $2.to_i
@@ -1048,11 +1109,6 @@ class Board
       y1 = $5.to_i
       name = $6
     elsif (str =~ /^%KACHI/)
-      if (@sente == @current_player)
-        sente = true
-      else
-        sente = false
-      end
       if (good_kachi?(sente))
         return :kachi_win
       else
@@ -1166,6 +1222,73 @@ class Board
   end
 end
 
+#################################################
+# Rating module
+#
+# http://www2.saganet.ne.jp/a-sim/mhp0726.html
+# http://www10.plala.or.jp/greenstone/content1_3.html
+class Rating
+  K = 16
+
+  def new_rate(me, you, win)
+    w = win ? 1 : 0
+    
+    if me == nil && you != nil
+      return (you + w*400).to_i
+    elsif me == nil && you == nil
+      return (1100 + w*400).to_i
+    elsif you == nil
+      return me.to_i
+    end
+    score = me + K*(w - we(me, you))
+    score.to_i
+  end
+
+  private
+
+  # win expectancy
+  def we(me, you)
+    dr = (me - you)
+    1.0 / ( 10**(-1.0*dr/400) + 1 )
+  end
+end
+
+
+
+class GameResult
+  def initialize(p1, p2)
+    @players = []
+    @players << p1
+    @players << p2
+  end
+end
+
+class GameResultWin < GameResult
+  attr_reader :winner, :loser
+    
+  def initialize(winner, loser)
+    super
+    @winner, @loser = winner, loser
+    rate if @winner.id && @loser.id
+  end
+
+  def win?(player)
+    @winner == player
+  end
+
+  def rate
+    rating = Rating.new
+    new_winner = rating.new_rate(@winner.rate, @loser.rate,  true)
+    new_loser  = rating.new_rate(@loser.rate,  @winner.rate, false)
+    @winner.rate = new_winner if new_winner
+    @loser.rate  = new_loser  if new_loser
+  end
+end
+
+class GameResultDraw < GameResult
+
+end
+
 class Game
   @@mutex = Mutex.new
   @@time  = 0
@@ -1209,11 +1332,13 @@ class Game
     @board.initial
     @start_time = nil
     @fh = nil
+    @result = nil
 
     propose
   end
   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
   attr_accessor :last_move, :current_turn
+  attr_reader   :result
 
   def monitoron(monitor)
     @monitors.delete(monitor)
@@ -1261,6 +1386,7 @@ class Game
     @current_player = nil
     @next_player = nil
     LEAGUE.games.delete(@id)
+    LEAGUE.save
   end
 
   def handle_one_move(str, player)
@@ -1282,7 +1408,7 @@ class Game
         end
 
 #        begin
-          move_status = @board.handle_one_move(str)
+          move_status = @board.handle_one_move(str, @sente == @current_player)
 #        rescue
 #          log_error("handle_one_move raise exception for #{str}")
 #          move_status = :illegal
@@ -1343,6 +1469,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
+    @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("%%TORYO\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
@@ -1356,6 +1483,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("%%TORYO\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
@@ -1369,6 +1497,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
+    @result = GameResultDraw.new(@current_player, @next_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1381,6 +1510,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1393,6 +1523,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1405,6 +1536,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1417,6 +1549,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1429,6 +1562,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#TIME_UP\n#LOSE\n")
     @next_player.write_safe("#TIME_UP\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1441,6 +1575,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
+    @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("%%KACHI\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
@@ -1454,6 +1589,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
+    @result = GameResultWin(@next_player, @current_player)
     @fh.printf("%%KACHI\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
@@ -1467,6 +1603,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
+    @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("%%TORYO\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
@@ -1480,6 +1617,7 @@ class Game
     @next_player.status = "connected"
     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
+    @result = GameResultWin.new(@current_player, @next_player)
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
     @monitors.each do |monitor|
@@ -1609,6 +1747,10 @@ EOM
   end
 end
 
+#################################################
+# MAIN
+#
+
 def usage
     print <<EOM
 NAME
@@ -1680,7 +1822,8 @@ def good_game_name?(str)
 end
 
 def good_identifier?(str)
-  if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
+  if ( ((str =~ /\A[\w\d_@\-\.]+\z/) || 
+        (str =~ /\A[\w\d_@\-\.]+,[\w\d_@\-\.]+\z/)) &&
       (str.length < Max_Identifier_Length))
     return true
   else
@@ -1741,8 +1884,6 @@ def main
 
   write_pid_file($options["pid-file"]) if ($options["pid-file"])
 
-
-
   server = TCPserver.open(port)
   log_message("server started")