OSDN Git Service

Removed the test file
[shogi-server/shogi-server.git] / shogi-server
index 5569a28..6a77f86 100755 (executable)
@@ -18,6 +18,7 @@
 ## along with this program; if not, write to the Free Software
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
+require 'kconv'
 require 'getoptlong'
 require 'thread'
 require 'timeout'
@@ -28,52 +29,19 @@ require 'digest/md5'
 require 'webrick'
 require 'fileutils'
 
-
-class TCPSocket
-  def gets_timeout(t = Default_Timeout)
-    begin
-      timeout(t) do
-        return self.gets
-      end
-    rescue TimeoutError
-      return nil
-    rescue
-      return nil
-    end
-  end
-  def gets_safe(t = nil)
-    if (t && t > 0)
-      begin
-        timeout(t) do
-          return self.gets
-        end
-      rescue TimeoutError
-        return :timeout
-      rescue Exception => ex
-        log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
-        return :exception
-      end
-    else
-      begin
-        return self.gets
-      rescue
-        return nil
-      end
-    end
-  end
-  def write_safe(str)
-    begin
-      return self.write(str)
-    rescue
-      return nil
-    end
+def gets_safe(socket, timeout=nil)
+  if r = select([socket], nil, nil, timeout)
+    return r[0].first.gets
+  else
+    return :timeout
   end
+rescue Exception => ex
+  log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+  return :exception
 end
 
-
 module ShogiServer # for a namespace
 
-Max_Write_Queue_Size = 1000
 Max_Identifier_Length = 32
 Default_Timeout = 60            # for single socket operation
 
@@ -87,8 +55,6 @@ Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
 Release.concat("-") if (Release == "")
 Revision = "$Revision$".gsub(/[^\.\d]/, '')
 
-
-
 class League
 
   class Floodgate
@@ -109,13 +75,14 @@ class League
         Thread.pass
         while (true)
           begin
-            sleep(20)
+            sleep(10)
             next if Time.now < @next_time
+            @league.reload
             match_game
             charge
           rescue Exception => ex 
             # ignore errors
-            log_error("[in Floodgate's thread] #{ex}")
+            log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
           end
         end
       end
@@ -148,38 +115,85 @@ class League
         Floodgate.game_name?(pl.game_name) &&
         pl.sente == nil
       end
-      random_match(players)
+      log_warning("DEBUG: %s" % [File.join(File.dirname(__FILE__), "pairing.rb")])
+      load File.join(File.dirname(__FILE__), "pairing.rb")
+      Pairing.default_pairing.match(players)
     end
+  end # class Floodgate
 
-    def random_match(players)
-      if players.size < 2
-        log_message("Floodgate: too few players [%d]" % [players.size])
-        return
+  #
+  # This manages those players who have their player_id.
+  # Since mk_rate mainly updates the yaml file, basically,
+  # this only reads data. But this writes some properties.
+  # TODO Such data should be facoted out to another file
+  #
+  class Persistent
+    def initialize(filename)
+      @db = YAML::Store.new(filename)
+      @db.transaction do |pstore|
+        @db['players'] ||= Hash.new
       end
-      log_message("Floodgate: found %d players. Making games..." % [players.size])
-      random_players = players.sort{ rand < 0.5 ? 1 : -1 }
-      pairs = [[random_players.shift]]
-      while !random_players.empty? do
-        if pairs.last.size < 2
-          pairs.last << random_players.shift
-        else
-          pairs << [random_players.shift]
-        end 
+    end
+
+    #
+    # trancaction=true means read only
+    #
+    def each_group(transaction=false)
+      @db.transaction(transaction) do
+        groups = @db["players"] || Hash.new
+        groups.each do |group, players|
+          yield group,players
+        end
       end
-      if pairs.last.size < 2
-        pairs.pop
+    end
+
+    def load_player(player)
+      return unless player.player_id
+
+      hash = nil
+      each_group(true) do |group, players|
+        hash = players[player.player_id]
+        break if hash
       end
-      pairs.each do |pair|
-        start_game(pair.first, pair.last)
+      return unless hash
+
+      # a current user
+      player.name          = hash['name']
+      player.rate          = hash['rate'] || 0
+      player.modified_at   = hash['last_modified']
+      player.rating_group  = hash['rating_group']
+      player.win           = hash['win']  || 0
+      player.loss          = hash['loss'] || 0
+      player.last_game_win = hash['last_game_win'] || false
+    end
+
+    def save(player)
+      return unless player.player_id
+
+      each_group do |group, players|
+        hash = players[player.player_id]
+        if hash
+          # write only this property. 
+          # the others are updated by ./mk_rate
+          hash['last_game_win'] = player.last_game_win
+          break
+        end
       end
     end
 
-    def start_game(p1, p2)
-      p1.sente = true
-      p2.sente = false
-      Game.new(p1.game_name, p1, p2)
+    def get_players
+      players = []
+      each_group(true) do |group, players_hash|
+        players << players_hash.keys
+      end
+      return players.flatten.collect do |player_id|
+        p = BasicPlayer.new
+        p.player_id = player_id
+        load_player(p)
+        p
+      end
     end
-  end # class Floodgate
+  end # class Persistent
 
   def initialize
     @mutex = Mutex.new # guard @players
@@ -193,27 +207,42 @@ class League
   attr_accessor :players, :games, :event, :dir
 
   def shutdown
+    @mutex.synchronize do
+      @players.each do |name, player| 
+        @persistent.save(player)
+      end
+    end
     @floodgate.shutdown
   end
 
   # this should be called just after instanciating a League object.
   def setup_players_database
-    @db = YAML::Store.new(File.join(@dir, "players.yaml"))
+    filename = File.join(@dir, "players.yaml")
+    @persistent = Persistent.new(filename)
   end
 
   def add(player)
-    self.load(player) if player.id
+    @persistent.load_player(player)
     @mutex.synchronize do
       @players[player.name] = player
     end
   end
   
   def delete(player)
+    @persistent.save(player)
     @mutex.synchronize do
       @players.delete(player.name)
     end
   end
 
+  def reload
+    @mutex.synchronize do
+      @players.each do |name, player| 
+        @persistent.load_player(player)
+      end
+    end
+  end
+
   def find_all_players
     found = nil
     @mutex.synchronize do
@@ -224,6 +253,14 @@ class League
     return found.map {|a| a.last}
   end
   
+  def find(player_name)
+    found = nil
+    @mutex.synchronize do
+      found = @players[player_name]
+    end
+    return found
+  end
+
   def get_player(status, game_name, sente, searcher)
     found = nil
     @mutex.synchronize do
@@ -239,43 +276,8 @@ class League
     return found ? found.last : nil
   end
   
-  def load(player)
-    hash = search(player.id)
-    if hash
-      # a current user
-      player.name         = hash['name']
-      player.rate         = hash['rate']
-      player.modified_at  = hash['last_modified']
-      player.rating_group = hash['rating_group']
-    end
-  end
-
-  def search(id)
-    hash = nil
-    @db.transaction do
-      break unless  @db["players"]
-      @db["players"].each do |group, players|
-        hash = players[id]
-        break if hash
-      end
-    end
-    hash
-  end
-
   def rated_players
-    players = []
-    @db.transaction(true) do
-      break unless  @db["players"]
-      @db["players"].each do |group, players_hash|
-        players << players_hash.keys
-      end
-    end
-    return players.flatten.collect do |id|
-      p = BasicPlayer.new
-      p.id = id
-      self.load(p)
-      p
-    end
+    return @persistent.get_players
   end
 end
 
@@ -286,7 +288,8 @@ end
 class Login
   def Login.good_login?(str)
     tokens = str.split
-    if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
+    if (((tokens.length == 3) || 
+        ((tokens.length == 4) && tokens[3] == "x1")) &&
         (tokens[0] == "LOGIN") &&
         (good_identifier?(tokens[1])))
       return true
@@ -313,7 +316,7 @@ class Login
 
   def Login.factory(str, player)
     (login, player.name, password, ext) = str.chomp.split
-    if (ext)
+    if ext
       return Loginx1.new(player, password)
     else
       return LoginCSA.new(player, password)
@@ -401,8 +404,18 @@ end
 
 
 class BasicPlayer
+  def initialize
+    @player_id = nil
+    @name = nil
+    @password = nil
+    @rate = 0
+    @win  = 0
+    @loss = 0
+    @last_game_win = false
+  end
+
   # Idetifier of the player in the rating system
-  attr_accessor :id
+  attr_accessor :player_id
 
   # Name of the player
   attr_accessor :name
@@ -412,6 +425,9 @@ class BasicPlayer
 
   # Score in the rating sysem
   attr_accessor :rate
+
+  # Number of games for win and loss in the rating system
+  attr_accessor :win, :loss
   
   # Group in the rating system
   attr_accessor :rating_group
@@ -419,10 +435,8 @@ class BasicPlayer
   # Last timestamp when the rate was modified
   attr_accessor :modified_at
 
-  def initialize
-    @name = nil
-    @password = nil
-  end
+  # Whether win the previous game or not
+  attr_accessor :last_game_win
 
   def modified_at
     @modified_at || Time.now
@@ -436,10 +450,14 @@ class BasicPlayer
   end
 
   def rated?
-    @id != nil
+    @player_id != nil
+  end
+
+  def last_game_win?
+    return @last_game_win
   end
 
-  def simple_id
+  def simple_player_id
     if @trip
       simple_name = @name.gsub(/@.*?$/, '')
       "%s+%s" % [simple_name, @trip[0..8]]
@@ -449,42 +467,40 @@ class BasicPlayer
   end
 
   ##
-  # Parses str in the LOGIN command, sets up @id and @trip
+  # Parses str in the LOGIN command, sets up @player_id and @trip
   #
   def set_password(str)
     if str && !str.empty?
       @password = str.strip
-      @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
+      @player_id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
     else
-      @id = @password = nil
+      @player_id = @password = nil
     end
   end
 end
 
 
 class Player < BasicPlayer
-  def initialize(str, socket)
+  def initialize(str, socket, eol=nil)
     super()
     @socket = socket
-    @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
+    @status = "connected"       # game_waiting -> agree_waiting -> start_waiting -> game -> finished
 
     @protocol = nil             # CSA or x1
-    @eol = "\m"                 # favorite eol code
+    @eol = eol || "\m"          # favorite eol code
     @game = nil
     @game_name = ""
     @mytime = 0                 # set in start method also
     @sente = nil
-    @write_queue = Queue::new
+    @socket_buffer = []
     @main_thread = Thread::current
-    @writer_thread = Thread::start do
-      Thread.pass
-      writer()
-    end
+    @mutex_write_guard = Mutex.new
   end
 
   attr_accessor :socket, :status
   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
-  attr_accessor :main_thread, :writer_thread, :write_queue
+  attr_accessor :main_thread
+  attr_reader :socket_buffer
   
   def kill
     log_message(sprintf("user %s killed", @name))
@@ -499,8 +515,6 @@ class Player < BasicPlayer
     if (@status != "finished")
       @status = "finished"
       log_message(sprintf("user %s finish", @name))    
-      # TODO you should confirm that there is no message in the queue.
-      Thread::kill(@writer_thread) if @writer_thread
       begin
 #        @socket.close if (! @socket.closed?)
       rescue
@@ -510,26 +524,25 @@ class Player < BasicPlayer
   end
 
   def write_safe(str)
-    @write_queue.push(str.gsub(/[\r\n]+/, @eol))
-  end
-
-  def writer
-    while (str = @write_queue.pop)
+    @mutex_write_guard.synchronize do
       begin
-        @socket.write(str)
+        if @socket.closed?
+          log_warning("%s's socket has been closed." % [@name])
+          return
+        end
+        if r = select(nil, [@socket], nil, 20)
+          r[1].first.write(str)
+        else
+          log_error("Sending a message to #{@name} timed up.")
+        end
       rescue Exception => ex
-        log_error("Failed to send a message to #{@name}.")
-        log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
-        return
+        log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
       end
     end
   end
 
   def to_s
-    if ((status == "game_waiting") ||
-        (status == "start_waiting") ||
-        (status == "agree_waiting") ||
-        (status == "game"))
+    if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
       if (@sente)
         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
       elsif (@sente == false)
@@ -543,29 +556,25 @@ class Player < BasicPlayer
   end
 
   def run(csa_1st_str=nil)
-    while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
+    while ( csa_1st_str || 
+            str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
+      $mutex.lock
       begin
-        $mutex.lock
-
-        if (@writer_thread == nil || @writer_thread.status == false)
-          # The writer_thread has been killed because of socket errors.
-          return
+        if (@game && @game.turn?(self))
+          @socket_buffer << str
+          str = @socket_buffer.shift
         end
+        log_message("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")]) if $DEBUG
 
         if (csa_1st_str)
           str = csa_1st_str
           csa_1st_str = nil
         end
-        if (@write_queue.size > Max_Write_Queue_Size)
-          log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
-               return
-        end
 
         if (@status == "finished")
           return
         end
         str.chomp! if (str.class == String) # may be strip! ?
-        log_message(str) if $DEBUG
         case str 
         when "" 
           # Application-level protocol for Keep-Alive
@@ -578,10 +587,10 @@ class Player < BasicPlayer
             move = array_str.shift
             additional = array_str.shift
             if /^'(.*)/ =~ additional
-              comment = array_str.unshift("'*#{$1}")
+              comment = array_str.unshift("'*#{$1.toeuc}")
             end
             s = @game.handle_one_move(move, self)
-            @game.fh.print("#{comment}\n") if (comment && !s)
+            @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
             return if (s && @protocol == LoginCSA::PROTOCOL)
           end
         when /^%[^%]/, :timeout
@@ -644,7 +653,7 @@ class Player < BasicPlayer
           players = LEAGUE.rated_players
           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
             write_safe("##[RATING] %s \t %4d @%s\n" % 
-                       [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
+                       [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
           end
           write_safe("##[RATING] +OK\n")
         when /^%%VERSION/
@@ -674,7 +683,7 @@ class Player < BasicPlayer
           rival = nil
           if (League::Floodgate.game_name?(game_name))
             if (my_sente_str != "*")
-              write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n"), my_sente_str, game_name)
+              write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name))
               next
             end
             @sente = nil
@@ -778,7 +787,8 @@ class Player < BasicPlayer
 end # class
 
 class Piece
-  PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
+  PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", 
+             "GI" => "NG", "KA" => "UM", "HI" => "RY"}
   def initialize(board, x, y, sente, promoted=false)
     @board = board
     @x = x
@@ -1161,6 +1171,7 @@ class Board
     @gote_history  = Hash::new(0)
     @array = [[], [], [], [], [], [], [], [], [], []]
     @move_count = 0
+    @teban = nil # black => true, white => false
   end
   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
   attr_reader :move_count
@@ -1195,6 +1206,7 @@ class Board
     (1..9).each do |i|
       PieceFU::new(self, i, 7, true)
     end
+    @teban = true
   end
 
   def have_piece?(hands, name)
@@ -1233,6 +1245,7 @@ class Board
       @array[x0][y0].move_to(x1, y1)
     end
     @move_count += 1
+    @teban = @teban ? false : true
     return true
   end
 
@@ -1286,7 +1299,7 @@ class Board
         return false
       end
     else                        # gote
-      if ((rival_ou.y != 0) &&
+      if ((rival_ou.y != 1) &&
           (@array[rival_ou.x][rival_ou.y - 1]) &&
           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
@@ -1296,9 +1309,8 @@ class Board
         return false
       end
     end
-    
+
     ## case: rival_ou is moving
-    escaped = false
     rival_ou.movable_grids.each do |(cand_x, cand_y)|
       tmp_board = Marshal.load(Marshal.dump(self))
       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
@@ -1316,17 +1328,30 @@ class Board
         if (@array[x][y] &&
             (@array[x][y].sente != sente) &&
             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
+          
+          names = []
           if (@array[x][y].promoted)
-            name = @array[x][y].promoted_name
+            names << @array[x][y].promoted_name
           else
-            name = @array[x][y].name
+            names << @array[x][y].name
+            if @array[x][y].promoted_name && 
+               @array[x][y].move_to?(fu_x, fu_y, @array[x][y].promoted_name)
+              names << @array[x][y].promoted_name 
+            end
           end
-          tmp_board = Marshal.load(Marshal.dump(self))
-          s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
-          raise "internal error" if (s != true)
-          if (! tmp_board.checkmated?(! sente)) # good move
-            return false
+          names.map! do |name|
+            tmp_board = Marshal.load(Marshal.dump(self))
+            s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
+            if s == :illegal
+              s # result
+            else
+              tmp_board.checkmated?(! sente) # result
+            end
           end
+          all_illegal = names.find {|a| a != :illegal}
+          raise "internal error: legal move not found" if all_illegal == nil
+          r = names.find {|a| a == false} # good move
+          return false if r == false # found good move
         end
         y = y + 1
       end
@@ -1469,7 +1494,6 @@ class Board
       return :illegal
     end
     
-
     if (sg == "+")
       sente = true if sente == nil           # deprecated
       return :illegal unless sente == true   # black player's move must be black
@@ -1512,7 +1536,6 @@ class Board
     end
 
     move_to(x0, y0, x1, y1, name, sente)
-    str = to_s
 
     update_sennichite(sente)
     return :normal
@@ -1551,7 +1574,7 @@ class Board
       end
       a.push("\n")
     end
-    a.push("+\n")
+    a.push("%s\n" % [@teban ? "+" : "-"])
     return a.join
   end
 end
@@ -1577,17 +1600,23 @@ class GameResultWin < GameResult
   def initialize(winner, loser)
     super
     @winner, @loser = winner, loser
+    @winner.last_game_win = true
+    @loser.last_game_win  = false
   end
 
   def to_s
-    black_name = @black.id || @black.name
-    white_name = @white.id || @white.name
+    black_name = @black.player_id || @black.name
+    white_name = @white.player_id || @white.name
     "%s:%s" % [black_name, white_name]
   end
 end
 
 class GameResultDraw < GameResult
-
+  def initialize(p1, p2)
+    super
+    p1.last_game_win = false
+    p2.last_game_win = false
+  end
 end
 
 class Game
@@ -1603,31 +1632,37 @@ class Game
     end
 
     if (player0.sente)
-      @sente = player0
-      @gote = player1
+      @sente, @gote = player0, player1
     else
-      @sente = player1
-      @gote = player0
+      @sente, @gote = player1, player0
     end
-    @current_player = @sente
-    @next_player = @gote
-
+    @sente.socket_buffer.clear
+    @gote.socket_buffer.clear
+    @current_player, @next_player = @sente, @gote
     @sente.game = self
-    @gote.game = self
+    @gote.game  = self
 
     @last_move = ""
     @current_turn = 0
 
     @sente.status = "agree_waiting"
-    @gote.status = "agree_waiting"
+    @gote.status  = "agree_waiting"
+
+    @game_id = sprintf("%s+%s+%s+%s+%s", 
+                  LEAGUE.event, @game_name, 
+                  @sente.name, @gote.name, issue_current_time)
     
-    @id = sprintf("%s+%s+%s+%s+%s", 
-                  LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
-    @logfile = File.join(LEAGUE.dir, @id + ".csa")
+    now = Time.now
+    log_dir_name = File.join(LEAGUE.dir, 
+                             now.strftime("%Y"),
+                             now.strftime("%m"),
+                             now.strftime("%d"))
+    FileUtils.mkdir_p(log_dir_name) unless File.exist?(log_dir_name)
+    @logfile = File.join(log_dir_name, @game_id + ".csa")
 
-    LEAGUE.games[@id] = self
+    LEAGUE.games[@game_id] = self
 
-    log_message(sprintf("game created %s", @id))
+    log_message(sprintf("game created %s", @game_id))
 
     @board = Board::new
     @board.initial
@@ -1637,7 +1672,7 @@ class Game
 
     propose
   end
-  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
+  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
   attr_accessor :last_move, :current_turn
   attr_reader   :result
 
@@ -1645,6 +1680,10 @@ class Game
     @sente.rated? && @gote.rated?
   end
 
+  def turn?(player)
+    return player.status == "game" && @current_player == player
+  end
+
   def monitoron(monitor)
     @monitors.delete(monitor)
     @monitors.push(monitor)
@@ -1655,13 +1694,13 @@ class Game
   end
 
   def reject(rejector)
-    @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
-    @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
+    @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
+    @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
     finish
   end
 
   def kill(killer)
-    if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
+    if ["agree_waiting", "start_waiting"].include?(@sente.status)
       reject(killer.name)
     elsif (@current_player == killer)
       abnormal_lose()
@@ -1670,7 +1709,7 @@ class Game
   end
 
   def finish
-    log_message(sprintf("game finished %s", @id))
+    log_message(sprintf("game finished %s", @game_id))
     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
     @fh.close
 
@@ -1690,84 +1729,89 @@ class Game
     @gote = nil
     @current_player = nil
     @next_player = nil
-    LEAGUE.games.delete(@id)
+    LEAGUE.games.delete(@game_id)
   end
 
+  # class Game
   def handle_one_move(str, player)
+    unless turn?(player)
+      return false if str == :timeout
+
+      @fh.puts("'Deferred %s" % [str])
+      log_warning("Deferred a move [%s] scince it is not %s 's turn." %
+                  [str, player.name])
+      player.socket_buffer << str # always in the player's thread
+      return nil
+    end
+
     finish_flag = true
-    if (@current_player == player)
-      @end_time = Time::new
-      t = (@end_time - @start_time).floor
-      t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
-      
-      move_status = nil
-      if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
-        status = :timeout
-      elsif (str == :timeout)
-        return false            # time isn't expired. players aren't swapped. continue game
-      else
-        @current_player.mytime = @current_player.mytime - t
-        if (@current_player.mytime < 0)
-          @current_player.mytime = 0
-        end
+    @end_time = Time::new
+    t = [(@end_time - @start_time).floor, Least_Time_Per_Move].max
+    
+    move_status = nil
+    if ((@current_player.mytime - t <= -@byoyomi) && 
+        ((@total_time > 0) || (@byoyomi > 0)))
+      status = :timeout
+    elsif (str == :timeout)
+      return false            # time isn't expired. players aren't swapped. continue game
+    else
+      @current_player.mytime -= t
+      if (@current_player.mytime < 0)
+        @current_player.mytime = 0
+      end
 
-#        begin
-          move_status = @board.handle_one_move(str, @sente == @current_player)
-#        rescue
-#          log_error("handle_one_move raise exception for #{str}")
-#          move_status = :illegal
-#        end
+      move_status = @board.handle_one_move(str, @sente == @current_player)
 
-        if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
-          @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
-        else
-          if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite_sente_lose) || (move_status == :oute_sennichite_gote_lose))
-            @sente.write_safe(sprintf("%s,T%d\n", str, t))
-            @gote.write_safe(sprintf("%s,T%d\n", str, t))
-            @fh.printf("%s\nT%d\n", str, t)
-            @last_move = sprintf("%s,T%d", str, t)
-            @current_turn = @current_turn + 1
-          end
+      if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
+        @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
+      else
+        if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status)
+          # Thinking time includes network traffic
+          @sente.write_safe(sprintf("%s,T%d\n", str, t))
+          @gote.write_safe(sprintf("%s,T%d\n", str, t))
+          @fh.printf("%s\nT%d\n", str, t)
+          @last_move = sprintf("%s,T%d", str, t)
+          @current_turn += 1
+        end
 
-          @monitors.each do |monitor|
-            monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
-            monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
-          end
+        @monitors.each do |monitor|
+          monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
+          monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @game_id))
         end
       end
+    end
 
-      if (@next_player.status != "game") # rival is logout or disconnected
-        abnormal_win()
-      elsif (status == :timeout)
-        timeout_lose()
-      elsif (move_status == :illegal)
-        illegal_lose()
-      elsif (move_status == :kachi_win)
-        kachi_win()
-      elsif (move_status == :kachi_lose)
-        kachi_lose()
-      elsif (move_status == :toryo)
-        toryo_lose()
-      elsif (move_status == :outori)
-        outori_win()
-      elsif (move_status == :oute_sennichite_sente_lose)
-        oute_sennichite_win_lose(@gote, @sente) # Sente is checking
-      elsif (move_status == :oute_sennichite_gote_lose)
-        oute_sennichite_win_lose(@sente, @gote) # Gote is checking
-      elsif (move_status == :sennichite)
-        sennichite_draw()
-      elsif (move_status == :uchifuzume)
-        uchifuzume_lose()
-      elsif (move_status == :oute_kaihimore)
-        oute_kaihimore_lose()
-      else
-        finish_flag = false
-      end
-      finish() if finish_flag
-      (@current_player, @next_player) = [@next_player, @current_player]
-      @start_time = Time::new
-      return finish_flag
+    if (@next_player.status != "game") # rival is logout or disconnected
+      abnormal_win()
+    elsif (status == :timeout)
+      timeout_lose()
+    elsif (move_status == :illegal)
+      illegal_lose()
+    elsif (move_status == :kachi_win)
+      kachi_win()
+    elsif (move_status == :kachi_lose)
+      kachi_lose()
+    elsif (move_status == :toryo)
+      toryo_lose()
+    elsif (move_status == :outori)
+      outori_win()
+    elsif (move_status == :oute_sennichite_sente_lose)
+      oute_sennichite_win_lose(@gote, @sente) # Sente is checking
+    elsif (move_status == :oute_sennichite_gote_lose)
+      oute_sennichite_win_lose(@sente, @gote) # Gote is checking
+    elsif (move_status == :sennichite)
+      sennichite_draw()
+    elsif (move_status == :uchifuzume)
+      uchifuzume_lose()
+    elsif (move_status == :oute_kaihimore)
+      oute_kaihimore_lose()
+    else
+      finish_flag = false
     end
+    finish() if finish_flag
+    @current_player, @next_player = @next_player, @current_player
+    @start_time = Time::new
+    return finish_flag
   end
 
   def abnormal_win
@@ -1781,7 +1825,7 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1796,7 +1840,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1810,7 +1854,7 @@ class Game
     @result = GameResultDraw.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @game_id))
     end
   end
 
@@ -1828,7 +1872,7 @@ class Game
     @result = GameResultWin.new(winner, loser)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @game_id))
     end
   end
 
@@ -1842,7 +1886,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1856,7 +1900,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1870,7 +1914,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1884,7 +1928,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @game_id))
     end
   end
 
@@ -1899,7 +1943,7 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id))
     end
   end
 
@@ -1914,7 +1958,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id))
     end
   end
 
@@ -1929,7 +1973,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1943,14 +1987,14 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
   def start
-    log_message(sprintf("game started %s", @id))
-    @sente.write_safe(sprintf("START:%s\n", @id))
-    @gote.write_safe(sprintf("START:%s\n", @id))
+    log_message(sprintf("game started %s", @game_id))
+    @sente.write_safe(sprintf("START:%s\n", @game_id))
+    @gote.write_safe(sprintf("START:%s\n", @game_id))
     @sente.mytime = @total_time
     @gote.mytime = @total_time
     @start_time = Time::new
@@ -1963,7 +2007,7 @@ class Game
     @fh.puts("V2")
     @fh.puts("N+#{@sente.name}")
     @fh.puts("N-#{@gote.name}")
-    @fh.puts("$EVENT:#{@id}")
+    @fh.puts("$EVENT:#{@game_id}")
 
     @sente.write_safe(propose_message("+"))
     @gote.write_safe(propose_message("-"))
@@ -1991,7 +2035,7 @@ Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
 Declaration:Jishogi 1.1
-Game_ID:#{@id}
+Game_ID:#{@game_id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
 Rematch_On_Draw:NO
@@ -2024,7 +2068,7 @@ Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
 Declaration:Jishogi 1.1
-Game_ID:#{@id}
+Game_ID:#{@game_id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
 Your_Turn:#{sg_flag}
@@ -2087,8 +2131,8 @@ DESCRIPTION
 OPTIONS
        --pid-file file
                specify filename for logging process ID
-    --daemon dir
-        run as a daemon. Log files will be put in dir.
+        --daemon dir
+                run as a daemon. Log files will be put in dir.
 
 LICENSE
        this file is distributed under GPL version2 and might be compiled by Exerb
@@ -2122,9 +2166,9 @@ end
 
 def parse_command_line
   options = Hash::new
-  parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
-                           ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
-                         )
+  parser = GetoptLong.new(
+    ["--daemon",   GetoptLong::REQUIRED_ARGUMENT],
+    ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
   parser.quiet = true
   begin
     parser.each_option do |name, arg|
@@ -2140,7 +2184,7 @@ end
 
 def write_pid_file(file)
   open(file, "w") do |fh|
-    fh.print Process::pid, "\n"
+    fh.puts "#{$$}"
   end
 end
 
@@ -2162,6 +2206,42 @@ def mutex_watchdog(mutex, sec)
   end
 end
 
+def login_loop(client)
+  player = login = nil
+  while r = select([client], nil, nil, ShogiServer::Login_Time) do
+    break unless str = r[0].first.gets
+    $mutex.lock # guards LEAGUE
+    begin
+      str =~ /([\r\n]*)$/
+      eol = $1
+      if (ShogiServer::Login::good_login?(str))
+        player = ShogiServer::Player::new(str, client, eol)
+        login  = ShogiServer::Login::factory(str, player)
+        if (current_player = LEAGUE.find(player.name))
+          if (current_player.password == player.password &&
+              current_player.status != "game")
+            log_message(sprintf("user %s login forcely", player.name))
+            current_player.kill
+          else
+            login.incorrect_duplicated_player(str)
+            player = nil
+            break
+          end
+        end
+        LEAGUE.add(player)
+        break
+      else
+        client.write("LOGIN:incorrect" + eol)
+        client.write("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
+      end
+    ensure
+      $mutex.unlock
+    end
+  end                       # login loop
+  return [player, login]
+end
+
 def main
 
   $mutex = Mutex::new
@@ -2179,15 +2259,13 @@ def main
   LEAGUE.event = ARGV.shift
   port = ARGV.shift
 
-  write_pid_file($options["pid-file"]) if ($options["pid-file"])
-
-  dir = $options["daemon"] || nil
+  dir = $options["daemon"]
   dir = File.expand_path(dir) if dir
   if dir && ! File.exist?(dir)
     FileUtils.mkdir(dir)
   end
   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
-  $logger = WEBrick::Log.new(log_file)
+  $logger = WEBrick::Log.new(log_file) # thread safe
 
   LEAGUE.dir = dir || File.dirname(__FILE__)
   LEAGUE.setup_players_database
@@ -2196,6 +2274,15 @@ def main
   config[:Port]       = port
   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
   config[:Logger]     = $logger
+  if $options["pid-file"]
+    pid_file = File.expand_path($options["pid-file"])
+    config[:StartCallback] = Proc.new do
+      write_pid_file(pid_file)
+    end
+    config[:StopCallback] = Proc.new do
+      FileUtils.rm(pid_file, :force => true)
+    end
+  end
 
   server = WEBrick::GenericServer.new(config)
   ["INT", "TERM"].each do |signal| 
@@ -2211,50 +2298,12 @@ def main
       # client.sync = true # this is already set in WEBrick 
       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
-      player = nil
-      login  = nil
-      while (str = client.gets_timeout(ShogiServer::Login_Time))
-        begin
-          $mutex.lock
-          str =~ /([\r\n]*)$/
-          eol = $1
-          if (ShogiServer::Login::good_login?(str))
-            player = ShogiServer::Player::new(str, client)
-            player.eol = eol
-            login  = ShogiServer::Login::factory(str, player)
-            if (LEAGUE.players[player.name])
-              if ((LEAGUE.players[player.name].password == player.password) &&
-                  (LEAGUE.players[player.name].status != "game"))
-                log_message(sprintf("user %s login forcely", player.name))
-                LEAGUE.players[player.name].kill
-              else
-                login.incorrect_duplicated_player(str)
-                #Thread::exit
-                #return
-                # TODO
-                player = nil
-                break
-              end
-            end
-            LEAGUE.add(player)
-            break
-          else
-            client.write_safe("LOGIN:incorrect" + eol)
-            client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
-          end
-        ensure
-          $mutex.unlock
-        end
-      end                       # login loop
-      if (! player)
-        #client.close
-        #Thread::exit
-        #return
-        next
-      end
+      player, login = login_loop(client) # loop
+      next unless player
+
       log_message(sprintf("user %s login", player.name))
       login.process
-      player.run(login.csa_1st_str)
+      player.run(login.csa_1st_str) # loop
       begin
         $mutex.lock
         if (player.game)
@@ -2274,7 +2323,7 @@ if ($0 == __FILE__)
   STDOUT.sync = true
   STDERR.sync = true
   TCPSocket.do_not_reverse_lookup = true
-  Thread.abort_on_exception = true
+  Thread.abort_on_exception = $DEBUG ? true : false
 
   LEAGUE = ShogiServer::League::new
   main