OSDN Git Service

When there were too few games to rate players (i.e. no
[shogi-server/shogi-server.git] / shogi-server
index 4782f70..abbb4d7 100755 (executable)
 ## along with this program; if not, write to the Free Software
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-Max_Write_Queue_Size = 1000
-Max_Identifier_Length = 32
-Default_Timeout = 60            # for single socket operation
-
-Default_Game_Name = "default-1500-0"
-
-One_Time = 10
-Least_Time_Per_Move = 1
-Login_Time = 300                # time for LOGIN
-
-Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
-Release.concat("-") if (Release == "")
-Revision = "$Revision$".gsub(/[^\.\d]/, '')
-
-STDOUT.sync = true
-STDERR.sync = true
-
 require 'getoptlong'
 require 'thread'
 require 'timeout'
 require 'socket'
-
-TCPSocket.do_not_reverse_lookup = true
+require 'yaml'
+require 'yaml/store'
+require 'digest/md5'
+require 'webrick'
+require 'fileutils'
 
 
 class TCPSocket
@@ -83,37 +69,284 @@ class TCPSocket
 end
 
 
+module ShogiServer # for a namespace
+
+Max_Write_Queue_Size = 1000
+Max_Identifier_Length = 32
+Default_Timeout = 60            # for single socket operation
+
+Default_Game_Name = "default-1500-0"
+
+One_Time = 10
+Least_Time_Per_Move = 1
+Login_Time = 300                # time for LOGIN
+
+Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
+Release.concat("-") if (Release == "")
+Revision = "$Revision$".gsub(/[^\.\d]/, '')
+
+
 class League
   def initialize
     @games = Hash::new
     @players = Hash::new
     @event = nil
+    @dir = File.dirname(__FILE__)
+  end
+  attr_accessor :players, :games, :event, :dir
+
+  # this should be called just after instanciating a League object.
+  def setup_players_database
+    @db = YAML::Store.new(File.join(@dir, "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) &&
           (player.game_name == game_name) &&
-          ((player.sente == nil) || (player.sente == sente)) &&
+          ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
           ((searcher == nil) || (player != searcher)))
         return player
       end
     end
     return 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
+  end
 end
 
-class Player
-  def initialize(str, socket)
+
+######################################################
+# Processes the LOGIN command.
+#
+class Login
+  def Login.good_login?(str)
+    tokens = str.split
+    if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
+        (tokens[0] == "LOGIN") &&
+        (good_identifier?(tokens[1])))
+      return true
+    else
+      return false
+    end
+  end
+
+  def Login.good_game_name?(str)
+    if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
+      return true
+    else
+      return false
+    end
+  end
+
+  def Login.good_identifier?(str)
+    if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
+      return true
+    else
+      return false
+    end
+  end
+
+  def Login.factory(str, player)
+    (login, player.name, password, ext) = str.chomp.split
+    if (ext)
+      return Loginx1.new(player, password)
+    else
+      return LoginCSA.new(player, password)
+    end
+  end
+
+  attr_reader :player
+  
+  # the first command that will be executed just after LOGIN.
+  # If it is nil, the default process will be started.
+  attr_reader :csa_1st_str
+
+  def initialize(player, password)
+    @player = player
+    @csa_1st_str = nil
+    parse_password(password)
+  end
+
+  def process
+    @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
+    log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol))
+  end
+
+  def incorrect_duplicated_player(str)
+    @player.write_safe("LOGIN:incorrect\n")
+    @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
+    sleep 3 # wait for sending the above messages.
+    @player.name = "%s [duplicated]" % [@player.name]
+    @player.finish
+  end
+end
+
+######################################################
+# Processes LOGIN for the CSA standard mode.
+#
+class LoginCSA < Login
+  PROTOCOL = "CSA"
+
+  def initialize(player, password)
+    @gamename = nil
+    super
+    @player.protocol = PROTOCOL
+  end
+
+  def parse_password(password)
+    if Login.good_game_name?(password)
+      @gamename = password
+      @player.set_password(nil)
+    elsif password.split(",").size > 1
+      @gamename, *trip = password.split(",")
+      @player.set_password(trip.join(","))
+    else
+      @player.set_password(password)
+      @gamename = Default_Game_Name
+    end
+    @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
+  end
+
+  def process
+    super
+    @csa_1st_str = "%%GAME #{@gamename} *"
+  end
+end
+
+######################################################
+# Processes LOGIN for the extented mode.
+#
+class Loginx1 < Login
+  PROTOCOL = "x1"
+
+  def initialize(player, password)
+    super
+    @player.protocol = PROTOCOL
+  end
+  
+  def parse_password(password)
+    @player.set_password(password)
+  end
+
+  def process
+    super
+    @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
+  end
+end
+
+
+class BasicPlayer
+  # Idetifier of the player in the rating system
+  attr_accessor :id
+
+  # Name of the player
+  attr_accessor :name
+  
+  # Password of the player, which does not include a trip
+  attr_accessor :password
+
+  # Score in the rating sysem
+  attr_accessor :rate
+  
+  # Group in the rating system
+  attr_accessor :rating_group
+
+  # Last timestamp when the rate was modified
+  attr_accessor :modified_at
+
+  def initialize
     @name = nil
     @password = nil
+  end
+
+  def modified_at
+    @modified_at || Time.now
+  end
+
+  def rate=(new_rate)
+    if @rate != new_rate
+      @rate = new_rate
+      @modified_at = Time.now
+    end
+  end
+
+  def rated?
+    @id != nil
+  end
+
+  def simple_id
+    if @trip
+      simple_name = @name.gsub(/@.*?$/, '')
+      "%s+%s" % [simple_name, @trip[0..8]]
+    else
+      @name
+    end
+  end
+
+  ##
+  # Parses str in the LOGIN command, sets up @id and @trip
+  #
+  def set_password(str)
+    if str && !str.empty?
+      @password = str.strip
+      @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
+    else
+      @id = @password = nil
+    end
+  end
+end
+
+
+class Player < BasicPlayer
+  def initialize(str, socket)
+    super()
     @socket = socket
     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
 
@@ -123,15 +356,18 @@ class Player
     @game_name = ""
     @mytime = 0                 # set in start method also
     @sente = nil
-    @writer_thread = nil
-    @main_thread = nil
     @write_queue = Queue::new
-    login(str)
+    @main_thread = Thread::current
+    @writer_thread = Thread::start do
+      Thread.pass
+      writer()
+    end
   end
 
-  attr_accessor :name, :password, :socket, :status
+  attr_accessor :socket, :status
   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
   attr_accessor :main_thread, :writer_thread, :write_queue
+  
   def kill
     log_message(sprintf("user %s killed", @name))
     if (@game)
@@ -145,9 +381,10 @@ class Player
     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?)
+#        @socket.close if (! @socket.closed?)
       rescue
         log_message(sprintf("user %s finish failed", @name))    
       end
@@ -185,32 +422,7 @@ class Player
     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
   end
 
-  def login(str)
-    str =~ /([\r\n]*)$/
-    @eol = $1
-    str.chomp!
-    (login, @name, @password, ext) = str.split
-    if (ext)
-      @protocol = "x1"
-    else
-      @protocol = "CSA"
-    end
-    @main_thread = Thread::current
-    @writer_thread = Thread::start do
-      writer()
-    end
-  end
-    
-  def run
-    write_safe(sprintf("LOGIN:%s OK\n", @name))
-    if (@protocol != "CSA")
-      log_message(sprintf("user %s run in %s mode", @name, @protocol))
-      write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
-    else
-      log_message(sprintf("user %s run in CSA mode", @name))
-      csa_1st_str = "%%GAME #{Default_Game_Name} *"
-    end
-    
+  def run(csa_1st_str=nil)
     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
       begin
         $mutex.lock
@@ -220,7 +432,7 @@ class Player
         end
         if (@write_queue.size > Max_Write_Queue_Size)
           log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
-         return
+               return
         end
 
         if (@status == "finished")
@@ -228,15 +440,27 @@ class Player
         end
         str.chomp! if (str.class == String)
         case str
-        when /^[\+\-%][^%]/, :timeout
+        when /^[\+\-][^%]/
+          if (@status == "game")
+            array_str = str.split(",")
+            move = array_str.shift
+            additional = array_str.shift
+            if /^'(.*)/ =~ additional
+              comment = array_str.unshift("'*#{$1}")
+            end
+            s = @game.handle_one_move(move, self)
+            @game.fh.print("#{comment}\n") if (comment && !s)
+            return if (s && @protocol == LoginCSA::PROTOCOL)
+          end
+        when /^%[^%]/, :timeout
           if (@status == "game")
             s = @game.handle_one_move(str, self)
-            return if (s && @protocol == "CSA")
+            return if (s && @protocol == LoginCSA::PROTOCOL)
           end
         when /^REJECT/
           if (@status == "agree_waiting")
             @game.reject(@name)
-            return if (@protocol == "CSA")
+            return if (@protocol == LoginCSA::PROTOCOL)
           else
             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
           end
@@ -272,14 +496,28 @@ class Player
           end
         when /^%%HELP/
           write_help
+        when /^%%RATING/
+          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")])
+          end
+          write_safe("##[RATING] +OK\n")
+        when /^%%VERSION/
+          write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
+          write_safe("##[VERSION] +OK\n")
         when /^%%GAME\s*$/
-          @status = "connected"
-          @game_name = ""
+          if ((@status == "connected") || (@status == "game_waiting"))
+            @status = "connected"
+            @game_name = ""
+          else
+            write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
+          end
         when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
           command_name = $1
           game_name = $2
           my_sente_str = $3
-          if (! good_game_name?(game_name))
+          if (! Login::good_game_name?(game_name))
             write_safe(sprintf("##[ERROR] bad game name\n"))
             next
           elsif ((@status == "connected") || (@status == "game_waiting"))
@@ -353,9 +591,8 @@ class Player
         when /^%%CHAT\s+(.+)/
           message = $1
           LEAGUE.players.each do |name, player|
-            if (player.protocol != "CSA")
-              s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
-              player.status = "zombie" if (! s)
+            if (player.protocol != LoginCSA::PROTOCOL)
+              player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
             end
           end
         when /^%%LIST/
@@ -379,7 +616,9 @@ class Player
         when /^\s*$/
           ## ignore null string
         else
-          write_safe(sprintf("##[ERROR] unknown command %s\n", str))
+          msg = "##[ERROR] unknown command %s\n" % [str]
+          write_safe(msg)
+          log_error(msg)
         end
       ensure
         $mutex.unlock
@@ -771,8 +1010,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)
@@ -853,6 +1094,7 @@ class Board
       end
       @array[x0][y0].move_to(x1, y1)
     end
+    @move_count += 1
     return true
   end
 
@@ -979,11 +1221,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
 
@@ -1010,16 +1262,28 @@ 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)
+  # sente is nil only if tests in test_board run
+  def handle_one_move(str, sente=nil)
     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
       sg = $1
       x0 = $2.to_i
@@ -1028,11 +1292,7 @@ class Board
       y1 = $5.to_i
       name = $6
     elsif (str =~ /^%KACHI/)
-      if (@sente == @current_player)
-        sente = true
-      else
-        sente = false
-      end
+      raise ArgumentError, "sente is null", caller if sente == nil
       if (good_kachi?(sente))
         return :kachi_win
       else
@@ -1051,11 +1311,14 @@ class Board
       return :illegal
     end
     
+
     if (sg == "+")
-      sente = true
+      sente = true if sente == nil           # deprecated
+      return :illegal unless sente == true   # black player's move must be black
       hands = @sente_hands
     else
-      sente = false
+      sente = false if sente == nil          # deprecated
+      return :illegal unless sente == false  # white player's move must be white
       hands = @gote_hands
     end
     
@@ -1146,7 +1409,46 @@ class Board
   end
 end
 
+class GameResult
+  attr_reader :players, :black, :white
+
+  def initialize(p1, p2)
+    @players = []
+    @players << p1
+    @players << p2
+    if p1.sente && !p2.sente
+      @black, @white = p1, p2
+    elsif !p1.sente && p2.sente
+      @black, @white = p2, p1
+    else
+      raise "Never reached!"
+    end
+  end
+end
+
+class GameResultWin < GameResult
+  attr_reader :winner, :loser
+
+  def initialize(winner, loser)
+    super
+    @winner, @loser = winner, loser
+  end
+
+  def to_s
+    black_name = @black.id || @black.name
+    white_name = @white.id || @white.name
+    "%s:%s" % [black_name, white_name]
+  end
+end
+
+class GameResultDraw < GameResult
+
+end
+
 class Game
+  @@mutex = Mutex.new
+  @@time  = 0
+
   def initialize(game_name, player0, player1)
     @monitors = Array::new
     @game_name = game_name
@@ -1173,25 +1475,30 @@ class Game
 
     @sente.status = "agree_waiting"
     @gote.status = "agree_waiting"
+    
     @id = sprintf("%s+%s+%s+%s+%s", 
-                  LEAGUE.event, @game_name, @sente.name, @gote.name,
-                  Time::new.strftime("%Y%m%d%H%M%S"))
+                  LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
+    @logfile = File.join(LEAGUE.dir, @id + ".csa")
 
     LEAGUE.games[@id] = self
 
-
     log_message(sprintf("game created %s", @id))
 
-    @logfile = @id + ".csa"
     @board = Board::new
     @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 rated?
+    @sente.rated? && @gote.rated?
+  end
 
   def monitoron(monitor)
     @monitors.delete(monitor)
@@ -1227,10 +1534,10 @@ class Game
     @sente.status = "connected"
     @gote.status = "connected"
 
-    if (@current_player.protocol == "CSA")
+    if (@current_player.protocol == LoginCSA::PROTOCOL)
       @current_player.finish
     end
-    if (@next_player.protocol == "CSA")
+    if (@next_player.protocol == LoginCSA::PROTOCOL)
       @next_player.finish
     end
     @monitors = Array::new
@@ -1245,21 +1552,27 @@ class Game
     finish_flag = true
     if (@current_player == player)
       @end_time = Time::new
-      t = (@end_time - @start_time).ceil
+      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 <= 0) && (@total_time > 0))
+      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
+
 #        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
 #        end
+
         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
         else
@@ -1270,6 +1583,7 @@ class Game
             @last_move = sprintf("%s,T%d", str, t)
             @current_turn = @current_turn + 1
           end
+
           @monitors.each do |monitor|
             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
@@ -1277,12 +1591,6 @@ class Game
         end
       end
 
-      if (@current_player.mytime - t < @byoyomi)
-        @current_player.mytime = @byoyomi
-      else
-        @current_player.mytime = @current_player.mytime - t
-      end
-
       if (@next_player.status != "game") # rival is logout or disconnected
         abnormal_win()
       elsif (status == :timeout)
@@ -1323,6 +1631,8 @@ class Game
     @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)
+    @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))
     end
@@ -1336,6 +1646,8 @@ class Game
     @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)
+    @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))
     end
@@ -1348,6 +1660,8 @@ class Game
     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1360,6 +1674,8 @@ class Game
     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
+    @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] #OUTE_SENNICHITE\n", @id))
     end
@@ -1372,6 +1688,8 @@ class Game
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1384,6 +1702,8 @@ class Game
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1396,6 +1716,8 @@ class Game
     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1408,6 +1730,8 @@ class Game
     @next_player.write_safe("#TIME_UP\n#WIN\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1421,6 +1745,8 @@ class Game
     @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)
+    @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))
     end
@@ -1434,6 +1760,8 @@ class Game
     @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)
+    @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))
     end
@@ -1447,6 +1775,8 @@ class Game
     @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)
+    @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))
     end
@@ -1459,6 +1789,8 @@ class Game
     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
     @fh.print(@board.to_s.gsub(/^/, "\'"))
     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
+    @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))
     end
@@ -1505,9 +1837,10 @@ EOM
   def show()
     str0 = <<EOM
 BEGIN Game_Summary
-Protocol_Version:1.0
+Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
+Declaration:Jishogi 1.1
 Game_ID:#{@id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
@@ -1524,7 +1857,6 @@ Last_Move:#{@last_move}
 Current_Turn:#{@current_turn}
 END Time
 BEGIN Position
-Jishogi_Declaration:1.1
 EOM
 
     str1 = <<EOM
@@ -1538,9 +1870,10 @@ EOM
   def propose_message(sg_flag)
     str = <<EOM
 BEGIN Game_Summary
-Protocol_Version:1.0
+Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
+Declaration:Jishogi 1.1
 Game_ID:#{@id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
@@ -1554,7 +1887,6 @@ Byoyomi:#{@byoyomi}
 Least_Time_Per_Move:#{Least_Time_Per_Move}
 END Time
 BEGIN Position
-Jishogi_Declaration:1.1
 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
 P2 * -HI *  *  *  *  * -KA * 
 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
@@ -1572,7 +1904,24 @@ END Game_Summary
 EOM
     return str
   end
+  
+  private
+  
+  def issue_current_time
+    time = Time::new.strftime("%Y%m%d%H%M%S").to_i
+    @@mutex.synchronize do
+      while time <= @@time do
+        time += 1
+      end
+      @@time = time
+    end
+  end
 end
+end # module ShogiServer
+
+#################################################
+# MAIN
+#
 
 def usage
     print <<EOM
@@ -1580,7 +1929,7 @@ NAME
        shogi-server - server for CSA server protocol
 
 SYNOPSIS
-       shogi-server event_name port_number
+       shogi-server [OPTIONS] event_name port_number
 
 DESCRIPTION
        server for CSA server protocol
@@ -1588,6 +1937,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.
 
 LICENSE
        this file is distributed under GPL version2 and might be compiled by Exerb
@@ -1595,33 +1946,31 @@ LICENSE
 SEE ALSO
 
 RELEASE
-       #{Release}
+       #{ShogiServer::Release}
 
 REVISION
-       #{Revision}
+       #{ShogiServer::Revision}
 EOM
 end
 
 def log_message(str)
-  printf("%s message: %s\n", Time::new.to_s, str)
+  $logger.info(str)
 end
 
 def log_warning(str)
-  printf("%s warning: %s\n", Time::new.to_s, str)
+  $logger.warn(str)
 end
 
 def log_error(str)
-  printf("%s error: %s\n", Time::new.to_s, str)
+  $logger.error(str)
 end
 
 
 def parse_command_line
   options = Hash::new
-  parser = GetoptLong.new
-  parser.ordering = GetoptLong::REQUIRE_ORDER
-  parser.set_options(
-                     ["--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|
@@ -1635,36 +1984,7 @@ def parse_command_line
   return options
 end
 
-def good_game_name?(str)
-  if ((str =~ /^(.+)-\d+-\d+$/) &&
-      (good_identifier?($1)))
-    return true
-  else
-    return false
-  end
-end
-
-def good_identifier?(str)
-  if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
-      (str.length < Max_Identifier_Length))
-    return true
-  else
-    return false
-  end
-end
-
-def good_login?(str)
-  tokens = str.split
-  if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
-      (tokens[0] == "LOGIN") &&
-      (good_identifier?(tokens[1])))
-    return true
-  else
-    return false
-  end
-end
-
-def  write_pid_file(file)
+def write_pid_file(file)
   open(file, "w") do |fh|
     fh.print Process::pid, "\n"
   end
@@ -1674,8 +1994,11 @@ def mutex_watchdog(mutex, sec)
   while true
     begin
       timeout(sec) do
-        mutex.lock
-        mutex.unlock
+        begin
+          mutex.lock
+        ensure
+          mutex.unlock
+        end
       end
       sleep(sec)
     rescue TimeoutError
@@ -1686,8 +2009,10 @@ def mutex_watchdog(mutex, sec)
 end
 
 def main
+
   $mutex = Mutex::new
   Thread::start do
+    Thread.pass
     mutex_watchdog($mutex, 10)
   end
 
@@ -1702,33 +2027,51 @@ def main
 
   write_pid_file($options["pid-file"]) if ($options["pid-file"])
 
+  dir = $options["daemon"] || nil
+  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)
 
-  Thread.abort_on_exception = true
+  LEAGUE.dir = dir || File.dirname(__FILE__)
+  LEAGUE.setup_players_database
 
-  server = TCPserver.open(port)
+  config = {}
+  config[:Port]       = port
+  config[:ServerType] = WEBrick::Daemon if $options["daemon"]
+  config[:Logger]     = $logger
+
+  server = WEBrick::GenericServer.new(config)
+  ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
+  $stderr.puts("server started as a deamon") if $options["daemon"] 
   log_message("server started")
 
-  while true
-    Thread::start(server.accept) do |client|
+  server.start do |client|
       client.sync = true
       player = nil
-      while (str = client.gets_timeout(Login_Time))
+      login  = nil
+      while (str = client.gets_timeout(ShogiServer::Login_Time))
         begin
           $mutex.lock
           str =~ /([\r\n]*)$/
           eol = $1
-          if (good_login?(str))
-            player = Player::new(str, client)
+          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
-                client.write_safe("LOGIN:incorrect" + eol)
-                client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
-                client.close
-                Thread::exit
+                login.incorrect_duplicated_player(str)
+                #Thread::exit
+                #return
+                # TODO
+                player = nil
+                break
               end
             end
             LEAGUE.add(player)
@@ -1742,27 +2085,35 @@ def main
         end
       end                       # login loop
       if (! player)
-        client.close
-        Thread::exit
+        #client.close
+        #Thread::exit
+        #return
+        next
       end
       log_message(sprintf("user %s login", player.name))
-      player.run
+      login.process
+      player.run(login.csa_1st_str)
       begin
         $mutex.lock
         if (player.game)
           player.game.kill(player)
         end
-        player.finish
+        player.finish # socket has been closed
         LEAGUE.delete(player)
         log_message(sprintf("user %s logout", player.name))
       ensure
         $mutex.unlock
       end
-    end
   end
 end
 
+
 if ($0 == __FILE__)
-  LEAGUE = League::new
+  STDOUT.sync = true
+  STDERR.sync = true
+  TCPSocket.do_not_reverse_lookup = true
+  Thread.abort_on_exception = true
+
+  LEAGUE = ShogiServer::League::new
   main
 end