OSDN Git Service

Improved a way to handle diferred moves (2008-03-24's change),
[shogi-server/shogi-server.git] / shogi-server
index ba7ad06..5d33d6f 100755 (executable)
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby
 #! /usr/bin/env ruby
-## -*-Ruby-*- $RCSfile$ $Revision$ $Name$
+## $Id$
 
 
-## Copyright (C) 2004 773@2ch
+## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
+## Copyright (C) 2007-2008 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
 ##
 ## 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
 ## along with this program; if not, write to the Free Software
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 ## along with this program; if not, write to the Free Software
 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
-DEFAULT_TIMEOUT = 10            # for single socket operation
-Total_Time = 1500
+require 'kconv'
+require 'getoptlong'
+require 'thread'
+require 'timeout'
+require 'socket'
+require 'yaml'
+require 'yaml/store'
+require 'digest/md5'
+require 'webrick'
+require 'fileutils'
+
+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_Identifier_Length = 32
+Default_Timeout = 60            # for single socket operation
+
+Default_Game_Name = "default-1500-0"
+
+One_Time = 10
 Least_Time_Per_Move = 1
 Least_Time_Per_Move = 1
-Watchdog_Time = 30              # time for ping
-Agree_Time = 300                # time for AGREE
 Login_Time = 300                # time for LOGIN
 
 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
 Release.concat("-") if (Release == "")
 Revision = "$Revision$".gsub(/[^\.\d]/, '')
 
 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
+class League
+
+  class Floodgate
+    class << self
+      def game_name?(str)
+        return /^floodgate-\d+-\d+$/.match(str) ? true : false
+      end
+    end
+
+    def initialize(league)
+      @league = league
+      @next_time = nil
+      charge
+    end
+
+    def run
+      @thread = Thread.new do
+        Thread.pass
+        while (true)
+          begin
+            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}")
+          end
+        end
+      end
+    end
+
+    def shutdown
+      @thread.kill if @thread
+    end
+
+    # private
+
+    def charge
+      now = Time.now
+      if now.min < 30
+        @next_time = Time.mktime(now.year, now.month, now.day, now.hour, 30)
+      else
+        @next_time = Time.mktime(now.year, now.month, now.day, now.hour) + 3600
+      end
+      # for test
+      # if now.sec < 30
+      #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min, 30)
+      # else
+      #   @next_time = Time.mktime(now.year, now.month, now.day, now.hour, now.min) + 60
+      # end
+    end
+
+    def match_game
+      players = @league.find_all_players do |pl|
+        pl.status == "game_waiting" &&
+        Floodgate.game_name?(pl.game_name) &&
+        pl.sente == nil
+      end
+      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 initialize
+    @mutex = Mutex.new # guard @players
+    @games = Hash::new
+    @players = Hash::new
+    @event = nil
+    @dir = File.dirname(__FILE__)
+    @floodgate = Floodgate.new(self)
+    @floodgate.run
+  end
+  attr_accessor :players, :games, :event, :dir
+
+  def shutdown
+    @mutex.synchronize do
+      @players.each {|a| save(a)}
+    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"))
+  end
+
+  def add(player)
+    self.load(player) if player.id
+    @mutex.synchronize do
+      @players[player.name] = player
+    end
+  end
+  
+  def delete(player)
+    @mutex.synchronize do
+      save(player)
+      @players.delete(player.name)
+    end
+  end
+
+  def reload
+    @mutex.synchronize do
+      @players.each {|player| load(player)}
+    end
+  end
+
+  def find_all_players
+    found = nil
+    @mutex.synchronize do
+      found = @players.find_all do |name, player|
+        yield player
+      end
+    end
+    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
+      found = @players.find do |name, player|
+        (player.status == status) &&
+        (player.game_name == game_name) &&
+        ( (sente == nil) || 
+          (player.sente == nil) || 
+          (player.sente == sente) ) &&
+        (player.name != searcher.name)
+      end
+    end
+    return found ? found.last : nil
+  end
+  
+  def load(player)
+    hash = search(player.id)
+    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)
+    @db.transaction do
+      break unless  @db["players"]
+      @db["players"].each do |group, players|
+        hash = players[player.id]
+        if hash
+          hash['last_game_win'] = player.last_game_win
+          break
+        end
+      end
+    end
+  end
+
+  def search(id)
+    hash = nil
+    @db.transaction(true) 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
+
+
+######################################################
+# 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
+  def initialize
+    @id = nil
+    @name = nil
+    @password = nil
+    @last_game_win = false
+  end
+
+  # 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
+
+  # Number of games for win and loss in the rating system
+  attr_accessor :win, :loss
+  
+  # Group in the rating system
+  attr_accessor :rating_group
+
+  # Last timestamp when the rate was modified
+  attr_accessor :modified_at
+
+  # Whether win the previous game or not
+  attr_accessor :last_game_win
+
+  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 last_game_win?
+    return @last_game_win
+  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, eol=nil)
+    super()
+    @socket = socket
+    @status = "connected"       # game_waiting -> agree_waiting -> start_waiting -> game -> finished
+
+    @protocol = nil             # CSA or x1
+    @eol = eol || "\m"          # favorite eol code
+    @game = nil
+    @game_name = ""
+    @mytime = 0                 # set in start method also
+    @sente = nil
+    @socket_buffer = []
+    @main_thread = Thread::current
+    @mutex_write_guard = Mutex.new
+  end
+
+  attr_accessor :socket, :status
+  attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
+  attr_accessor :main_thread
+  attr_reader :socket_buffer
+  
+  def kill
+    log_message(sprintf("user %s killed", @name))
+    if (@game)
+      @game.kill(self)
+    end
+    finish
+    Thread::kill(@main_thread) if @main_thread
+  end
+
+  def finish
+    if (@status != "finished")
+      @status = "finished"
+      log_message(sprintf("user %s finish", @name))    
+      begin
+#        @socket.close if (! @socket.closed?)
+      rescue
+        log_message(sprintf("user %s finish failed", @name))    
+      end
+    end
+  end
+
+  def write_safe(str)
+    @mutex_write_guard.synchronize do
+      begin
+        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}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
+      end
+    end
+  end
+
+  def to_s
+    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)
+        return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
+      elsif (@sente == nil)
+        return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
+      end
+    else
+      return sprintf("%s %s %s", @name, @protocol, @status)
+    end
+  end
+
+  def run(csa_1st_str=nil)
+    while ( csa_1st_str || 
+            str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
+      $mutex.lock
+      begin
+        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 (@status == "finished")
+          return
+        end
+        str.chomp! if (str.class == String) # may be strip! ?
+        case str 
+        when "" 
+          # Application-level protocol for Keep-Alive
+          # If the server gets LF, it sends back LF.
+          # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
+          write_safe("\n")
+        when /^[\+\-][^%]/
+          if (@status == "game")
+            array_str = str.split(",")
+            move = array_str.shift
+            additional = array_str.shift
+            if /^'(.*)/ =~ additional
+              comment = array_str.unshift("'*#{$1.toeuc}")
+            end
+            s = @game.handle_one_move(move, self)
+            @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
+            return if (s && @protocol == LoginCSA::PROTOCOL)
+          end
+        when /^%[^%]/, :timeout
+          if (@status == "game")
+            s = @game.handle_one_move(str, self)
+            return if (s && @protocol == LoginCSA::PROTOCOL)
+          # else
+          #   begin
+          #     @socket.write("##[KEEPALIVE] #{Time.now}\n")
+          #   rescue Exception => ex
+          #     log_error("Failed to send a keepalive to #{@name}.")
+          #     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
+          #     return
+          #   end
+          end
+        when :exception
+          log_error("Failed to receive a message from #{@name}.")
+          return
+        when /^REJECT/
+          if (@status == "agree_waiting")
+            @game.reject(@name)
+            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
+        when /^AGREE/
+          if (@status == "agree_waiting")
+            @status = "start_waiting"
+            if ((@game.sente.status == "start_waiting") &&
+                (@game.gote.status == "start_waiting"))
+              @game.start
+              @game.sente.status = "game"
+              @game.gote.status = "game"
+            end
+          else
+            write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
+          end
+        when /^%%SHOW\s+(\S+)/
+          game_id = $1
+          if (LEAGUE.games[game_id])
+            write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
+          end
+          write_safe("##[SHOW] +OK\n")
+        when /^%%MONITORON\s+(\S+)/
+          game_id = $1
+          if (LEAGUE.games[game_id])
+            LEAGUE.games[game_id].monitoron(self)
+            write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
+            write_safe("##[MONITOR][#{game_id}] +OK\n")
+          end
+        when /^%%MONITOROFF\s+(\S+)/
+          game_id = $1
+          if (LEAGUE.games[game_id])
+            LEAGUE.games[game_id].monitoroff(self)
+          end
+        when /^%%HELP/
+          write_safe(
+            %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
+        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*$/
+          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 (! Login::good_game_name?(game_name))
+            write_safe(sprintf("##[ERROR] bad game name\n"))
+            next
+          elsif ((@status == "connected") || (@status == "game_waiting"))
+            ## continue
+          else
+            write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
+            next
+          end
+
+          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))
+              next
+            end
+            @sente = nil
+          else
+            if (my_sente_str == "*")
+              rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
+            elsif (my_sente_str == "+")
+              rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
+            elsif (my_sente_str == "-")
+              rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
+            else
+              ## never reached
+              write_safe(sprintf("##[ERROR] bad game option\n"))
+              next
+            end
+          end
+
+          if (rival)
+            @game_name = game_name
+            if ((my_sente_str == "*") && (rival.sente == nil))
+              if (rand(2) == 0)
+                @sente = true
+                rival.sente = false
+              else
+                @sente = false
+                rival.sente = true
+              end
+            elsif (rival.sente == true) # rival has higher priority
+              @sente = false
+            elsif (rival.sente == false)
+              @sente = true
+            elsif (my_sente_str == "+")
+              @sente = true
+              rival.sente = false
+            elsif (my_sente_str == "-")
+              @sente = false
+              rival.sente = true
+            else
+              ## never reached
+            end
+            Game::new(@game_name, self, rival)
+          else # rival not found
+            if (command_name == "GAME")
+              @status = "game_waiting"
+              @game_name = game_name
+              if (my_sente_str == "+")
+                @sente = true
+              elsif (my_sente_str == "-")
+                @sente = false
+              else
+                @sente = nil
+              end
+            else                # challenge
+              write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
+              @status = "connected"
+              @game_name = ""
+              @sente = nil
+            end
+          end
+        when /^%%CHAT\s+(.+)/
+          message = $1
+          LEAGUE.players.each do |name, player|
+            if (player.protocol != LoginCSA::PROTOCOL)
+              player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
+            end
+          end
+        when /^%%LIST/
+          buf = Array::new
+          LEAGUE.games.each do |id, game|
+            buf.push(sprintf("##[LIST] %s\n", id))
+          end
+          buf.push("##[LIST] +OK\n")
+          write_safe(buf.join)
+        when /^%%WHO/
+          buf = Array::new
+          LEAGUE.players.each do |name, player|
+            buf.push(sprintf("##[WHO] %s\n", player.to_s))
+          end
+          buf.push("##[WHO] +OK\n")
+          write_safe(buf.join)
+        when /^LOGOUT/
+          @status = "connected"
+          write_safe("LOGOUT:completed\n")
+          return
+        when /^CHALLENGE/
+          # This command is only available for CSA's official testing server.
+          # So, this means nothing for this program.
+          write_safe("CHALLENGE ACCEPTED\n")
+        when /^\s*$/
+          ## ignore null string
+        else
+          msg = "##[ERROR] unknown command %s\n" % [str]
+          write_safe(msg)
+          log_error(msg)
+        end
+      ensure
+        $mutex.unlock
+      end
+    end # enf of while
+  end # def run
+end # class
+
+class Piece
+  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
+    @y = y
+    @sente = sente
+    @promoted = promoted
+
+    if ((x == 0) || (y == 0))
+      if (sente)
+        hands = board.sente_hands
+      else
+        hands = board.gote_hands
+      end
+      hands.push(self)
+      hands.sort! {|a, b|
+        a.name <=> b.name
+      }
+    else
+      @board.array[x][y] = self
+    end
+  end
+  attr_accessor :promoted, :sente, :x, :y, :board
+
+  def room_of_head?(x, y, name)
+    true
+  end
+
+  def movable_grids
+    return adjacent_movable_grids + far_movable_grids
+  end
+
+  def far_movable_grids
+    return []
+  end
+
+  def jump_to?(x, y)
+    if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
+      if ((@board.array[x][y] == nil) || # dst is empty
+          (@board.array[x][y].sente != @sente)) # dst is enemy
+        return true
+      end
+    end
+    return false
+  end
+
+  def put_to?(x, y)
+    if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
+      if (@board.array[x][y] == nil) # dst is empty?
+        return true
+      end
+    end
+    return false
+  end
+
+  def adjacent_movable_grids
+    grids = Array::new
+    if (@promoted)
+      moves = @promoted_moves
+    else
+      moves = @normal_moves
+    end
+    moves.each do |(dx, dy)|
+      if (@sente)
+        cand_y = @y - dy
+      else
+        cand_y = @y + dy
+      end
+      cand_x = @x + dx
+      if (jump_to?(cand_x, cand_y))
+        grids.push([cand_x, cand_y])
+      end
+    end
+    return grids
+  end
+
+  def move_to?(x, y, name)
+    return false if (! room_of_head?(x, y, name))
+    return false if ((name != @name) && (name != @promoted_name))
+    return false if (@promoted && (name != @promoted_name)) # can't un-promote
+
+    if (! @promoted)
+      return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
+      if (@sente)
+        return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
+      else
+        return false if ((6 >= @y) && (6 >= y) && (name != @name))
+      end
+    end
+
+    if ((@x == 0) || (@y == 0))
+      return jump_to?(x, y)
+    else
+      return movable_grids.include?([x, y])
+    end
+  end
+
+  def move_to(x, y)
+    if ((@x == 0) || (@y == 0))
+      if (@sente)
+        @board.sente_hands.delete(self)
+      else
+        @board.gote_hands.delete(self)
+      end
+      @board.array[x][y] = self
+    elsif ((x == 0) || (y == 0))
+      @promoted = false         # clear promoted flag before moving to hands
+      if (@sente)
+        @board.sente_hands.push(self)
+      else
+        @board.gote_hands.push(self)
+      end
+      @board.array[@x][@y] = nil
+    else
+      @board.array[@x][@y] = nil
+      @board.array[x][y] = self
+    end
+    @x = x
+    @y = y
+  end
+
+  def point
+    @point
+  end
+
+  def name
+    @name
+  end
+
+  def promoted_name
+    @promoted_name
+  end
+
+  def to_s
+    if (@sente)
+      sg = "+"
+    else
+      sg = "-"
+    end
+    if (@promoted)
+      n = @promoted_name
+    else
+      n = @name
+    end
+    return sg + n
+  end
+end
+
+class PieceFU < Piece
+  def initialize(*arg)
+    @point = 1
+    @normal_moves = [[0, +1]]
+    @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
+    @name = "FU"
+    @promoted_name = "TO"
+    super
+  end
+  def room_of_head?(x, y, name)
+    if (name == "FU")
+      if (@sente)
+        return false if (y == 1)
+      else
+        return false if (y == 9)
+      end
+      ## 2fu check
+      c = 0
+      iy = 1
+      while (iy <= 9)
+        if ((iy  != @y) &&      # not source position
+            @board.array[x][iy] &&
+            (@board.array[x][iy].sente == @sente) && # mine
+            (@board.array[x][iy].name == "FU") &&
+            (@board.array[x][iy].promoted == false))
+          return false
+        end
+        iy = iy + 1
+      end
+    end
+    return true
+  end
+end
+
+class PieceKY  < Piece
+  def initialize(*arg)
+    @point = 1
+    @normal_moves = []
+    @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
+    @name = "KY"
+    @promoted_name = "NY"
+    super
+  end
+  def room_of_head?(x, y, name)
+    if (name == "KY")
+      if (@sente)
+        return false if (y == 1)
+      else
+        return false if (y == 9)
+      end
+    end
+    return true
+  end
+  def far_movable_grids
+    grids = Array::new
+    if (@promoted)
+      return []
+    else
+      if (@sente)                 # up
+        cand_x = @x
+        cand_y = @y - 1
+        while (jump_to?(cand_x, cand_y))
+          grids.push([cand_x, cand_y])
+          break if (! put_to?(cand_x, cand_y))
+          cand_y = cand_y - 1
+        end
+      else                        # down
+        cand_x = @x
+        cand_y = @y + 1
+        while (jump_to?(cand_x, cand_y))
+          grids.push([cand_x, cand_y])
+          break if (! put_to?(cand_x, cand_y))
+          cand_y = cand_y + 1
+        end
+      end
+      return grids
+    end
+  end
+end
+class PieceKE  < Piece
+  def initialize(*arg)
+    @point = 1
+    @normal_moves = [[+1, +2], [-1, +2]]
+    @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
+    @name = "KE"
+    @promoted_name = "NK"
+    super
+  end
+  def room_of_head?(x, y, name)
+    if (name == "KE")
+      if (@sente)
+        return false if ((y == 1) || (y == 2))
+      else
+        return false if ((y == 9) || (y == 8))
+      end
+    end
+    return true
+  end
+end
+class PieceGI  < Piece
+  def initialize(*arg)
+    @point = 1
+    @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
+    @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
+    @name = "GI"
+    @promoted_name = "NG"
+    super
+  end
+end
+class PieceKI  < Piece
+  def initialize(*arg)
+    @point = 1
+    @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
+    @promoted_moves = []
+    @name = "KI"
+    @promoted_name = nil
+    super
+  end
+end
+class PieceKA  < Piece
+  def initialize(*arg)
+    @point = 5
+    @normal_moves = []
+    @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
+    @name = "KA"
+    @promoted_name = "UM"
+    super
+  end
+  def far_movable_grids
+    grids = Array::new
+    ## up right
+    cand_x = @x - 1
+    cand_y = @y - 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x - 1
+      cand_y = cand_y - 1
+    end
+    ## down right
+    cand_x = @x - 1
+    cand_y = @y + 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x - 1
+      cand_y = cand_y + 1
+    end
+    ## up left
+    cand_x = @x + 1
+    cand_y = @y - 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x + 1
+      cand_y = cand_y - 1
+    end
+    ## down left
+    cand_x = @x + 1
+    cand_y = @y + 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x + 1
+      cand_y = cand_y + 1
+    end
+    return grids
+  end
+end
+class PieceHI  < Piece
+  def initialize(*arg)
+    @point = 5
+    @normal_moves = []
+    @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
+    @name = "HI"
+    @promoted_name = "RY"
+    super
+  end
+  def far_movable_grids
+    grids = Array::new
+    ## up
+    cand_x = @x
+    cand_y = @y - 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_y = cand_y - 1
+    end
+    ## down
+    cand_x = @x
+    cand_y = @y + 1
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_y = cand_y + 1
+    end
+    ## right
+    cand_x = @x - 1
+    cand_y = @y
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x - 1
+    end
+    ## down
+    cand_x = @x + 1
+    cand_y = @y
+    while (jump_to?(cand_x, cand_y))
+      grids.push([cand_x, cand_y])
+      break if (! put_to?(cand_x, cand_y))
+      cand_x = cand_x + 1
+    end
+    return grids
+  end
+end
+class PieceOU < Piece
+  def initialize(*arg)
+    @point = 0
+    @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
+    @promoted_moves = []
+    @name = "OU"
+    @promoted_name = nil
+    super
+  end
+end
+
+class Board
+  def initialize
+    @sente_hands = Array::new
+    @gote_hands  = Array::new
+    @history       = Hash::new(0)
+    @sente_history = Hash::new(0)
+    @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
+
+  def initial
+    PieceKY::new(self, 1, 1, false)
+    PieceKE::new(self, 2, 1, false)
+    PieceGI::new(self, 3, 1, false)
+    PieceKI::new(self, 4, 1, false)
+    PieceOU::new(self, 5, 1, false)
+    PieceKI::new(self, 6, 1, false)
+    PieceGI::new(self, 7, 1, false)
+    PieceKE::new(self, 8, 1, false)
+    PieceKY::new(self, 9, 1, false)
+    PieceKA::new(self, 2, 2, false)
+    PieceHI::new(self, 8, 2, false)
+    (1..9).each do |i|
+      PieceFU::new(self, i, 3, false)
+    end
+
+    PieceKY::new(self, 1, 9, true)
+    PieceKE::new(self, 2, 9, true)
+    PieceGI::new(self, 3, 9, true)
+    PieceKI::new(self, 4, 9, true)
+    PieceOU::new(self, 5, 9, true)
+    PieceKI::new(self, 6, 9, true)
+    PieceGI::new(self, 7, 9, true)
+    PieceKE::new(self, 8, 9, true)
+    PieceKY::new(self, 9, 9, true)
+    PieceKA::new(self, 8, 8, true)
+    PieceHI::new(self, 2, 8, true)
+    (1..9).each do |i|
+      PieceFU::new(self, i, 7, true)
+    end
+    @teban = true
+  end
+
+  def have_piece?(hands, name)
+    piece = hands.find { |i|
+      i.name == name
+    }
+    return piece
+  end
+
+  def move_to(x0, y0, x1, y1, name, sente)
+    if (sente)
+      hands = @sente_hands
+    else
+      hands = @gote_hands
+    end
+
+    if ((x0 == 0) || (y0 == 0))
+      piece = have_piece?(hands, name)
+      return :illegal if (! piece.move_to?(x1, y1, name)) # TODO null check for the piece?
+      piece.move_to(x1, y1)
+    else
+      return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))  # TODO null check?
+      if (@array[x0][y0].name != name) # promoted ?
+        @array[x0][y0].promoted = true
+      end
+      if (@array[x1][y1]) # capture
+        if (@array[x1][y1].name == "OU")
+          return :outori        # return board update
+        end
+        @array[x1][y1].sente = @array[x0][y0].sente
+        @array[x1][y1].move_to(0, 0)
+        hands.sort! {|a, b| # TODO refactor. Move to Piece class
+          a.name <=> b.name
+        }
+      end
+      @array[x0][y0].move_to(x1, y1)
+    end
+    @move_count += 1
+    @teban = @teban ? false : true
+    return true
+  end
+
+  def look_for_ou(sente)
+    x = 1
+    while (x <= 9)
+      y = 1
+      while (y <= 9)
+        if (@array[x][y] &&
+            (@array[x][y].name == "OU") &&
+            (@array[x][y].sente == sente))
+          return @array[x][y]
+        end
+        y = y + 1
+      end
+      x = x + 1
+    end
+    raise "can't find ou"
+  end
+
+  # note checkmate, but check. sente is checked.
+  def checkmated?(sente)        # sente is loosing
+    ou = look_for_ou(sente)
+    x = 1
+    while (x <= 9)
+      y = 1
+      while (y <= 9)
+        if (@array[x][y] &&
+            (@array[x][y].sente != sente))
+          if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
+            return true
+          end
+        end
+        y = y + 1
+      end
+      x = x + 1
+    end
+    return false
+  end
+
+  def uchifuzume?(sente)
+    rival_ou = look_for_ou(! sente)   # rival's ou
+    if (sente)                  # rival is gote
+      if ((rival_ou.y != 9) &&
+          (@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
+        fu_x = rival_ou.x
+        fu_y = rival_ou.y + 1
+      else
+        return false
+      end
+    else                        # gote
+      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
+        fu_x = rival_ou.x
+        fu_y = rival_ou.y - 1
+      else
+        return false
+      end
+    end
 
 
-require 'getoptlong'
-require 'thread'
-require 'timeout'
-require 'socket'
-require 'ping'
+    ## case: rival_ou is moving
+    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)
+      raise "internal error" if (s != true)
+      if (! tmp_board.checkmated?(! sente)) # good move
+        return false
+      end
+    end
 
 
-TCPSocket.do_not_reverse_lookup = true
+    ## case: rival is capturing fu
+    x = 1
+    while (x <= 9)
+      y = 1
+      while (y <= 9)
+        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)
+            names << @array[x][y].promoted_name
+          else
+            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
+          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
+      x = x + 1
+    end
+    return true
+  end
 
 
-class TCPSocket
-  def gets_timeout(t = DEFAULT_TIMEOUT)
-    begin
-      timeout(t) do
-        return self.gets
+  # @[sente|gote]_history has at least one item while the player is checking the other or 
+  # the other escapes.
+  def update_sennichite(player)
+    str = to_s
+    @history[str] += 1
+    if checkmated?(!player)
+      if (player)
+        @sente_history["dummy"] = 1  # flag to see Sente player is checking Gote player
+      else
+        @gote_history["dummy"]  = 1  # flag to see Gote player is checking Sente player
       end
       end
-    rescue TimeoutError
-      return nil
-    rescue
-      return nil
+    else
+      if (player)
+        @sente_history.clear # no more continuous check
+      else
+        @gote_history.clear  # no more continuous check
+      end
+    end
+    if @sente_history.size > 0  # possible for Sente's or Gote's turn
+      @sente_history[str] += 1
+    end
+    if @gote_history.size > 0   # possible for Sente's or Gote's turn
+      @gote_history[str] += 1
     end
   end
     end
   end
-  def gets_safe
-    begin
-      return self.gets
-    rescue
+
+  def oute_sennichite?(player)
+    if (@sente_history[to_s] >= 4)
+      return :oute_sennichite_sente_lose
+    elsif (@gote_history[to_s] >= 4)
+      return :oute_sennichite_gote_lose
+    else
       return nil
     end
   end
       return nil
     end
   end
-  def write_safe(str)
-    begin
-      return self.write(str)
-    rescue
-      return nil
+
+  def sennichite?(sente)
+    if (@history[to_s] >= 4) # already 3 times
+      return true
     end
     end
+    return false
   end
   end
-end
 
 
+  def good_kachi?(sente)
+    if (checkmated?(sente))
+      puts "'NG: Checkmating." if $DEBUG
+      return false 
+    end
+    
+    ou = look_for_ou(sente)
+    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
 
 
-class League
-  def initialize
-    @hash = Hash::new
+    if (sente)
+      hands = @sente_hands
+      r = [1, 2, 3]
+    else
+      hands = @gote_hands
+      r = [7, 8, 9]
+    end
+    r.each do |y|
+      x = 1
+      while (x <= 9)
+        if (@array[x][y] &&
+            (@array[x][y].sente == sente) &&
+            (@array[x][y].point > 0))
+          point = point + @array[x][y].point
+          number = number + 1
+        end
+        x = x + 1
+      end
+    end
+    hands.each do |piece|
+      point = point + piece.point
+    end
+
+    if (number < 10)
+      puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
+      return false     
+    end  
+    if (sente)
+      if (point < 28)
+        puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
+        return false 
+      end  
+    else
+      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
   end
-  attr_accessor :hash
 
 
-  def add(player)
-    @hash[player.name] = player
+  # 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
+      y0 = $3.to_i
+      x1 = $4.to_i
+      y1 = $5.to_i
+      name = $6
+    elsif (str =~ /^%KACHI/)
+      raise ArgumentError, "sente is null", caller if sente == nil
+      if (good_kachi?(sente))
+        return :kachi_win
+      else
+        return :kachi_lose
+      end
+    elsif (str =~ /^%TORYO/)
+      return :toryo
+    else
+      return :illegal
+    end
+    
+    if (((x0 == 0) || (y0 == 0)) && # source is not from hand
+        ((x0 != 0) || (y0 != 0)))
+      return :illegal
+    elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
+      return :illegal
+    end
+    
+    if (sg == "+")
+      sente = true if sente == nil           # deprecated
+      return :illegal unless sente == true   # black player's move must be black
+      hands = @sente_hands
+    else
+      sente = false if sente == nil          # deprecated
+      return :illegal unless sente == false  # white player's move must be white
+      hands = @gote_hands
+    end
+    
+    ## source check
+    if ((x0 == 0) && (y0 == 0))
+      return :illegal if (! have_piece?(hands, name))
+    elsif (! @array[x0][y0])
+      return :illegal           # no piece
+    elsif (@array[x0][y0].sente != sente)
+      return :illegal           # this is not mine
+    elsif (@array[x0][y0].name != name)
+      return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
+    end
+
+    ## destination check
+    if (@array[x1][y1] &&
+        (@array[x1][y1].sente == sente)) # can't capture mine
+      return :illegal
+    elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
+      return :illegal           # can't put on existing piece
+    end
+
+    tmp_board = Marshal.load(Marshal.dump(self))
+    return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
+    return :oute_kaihimore if (tmp_board.checkmated?(sente))
+    tmp_board.update_sennichite(sente)
+    os_result = tmp_board.oute_sennichite?(sente)
+    return os_result if os_result # :oute_sennichite_sente_lose or :oute_sennichite_gote_lose
+    return :sennichite if tmp_board.sennichite?(sente)
+
+    if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
+      return :uchifuzume
+    end
+
+    move_to(x0, y0, x1, y1, name, sente)
+
+    update_sennichite(sente)
+    return :normal
   end
   end
-  def delete(player)
-    @hash.delete(player.name)
+
+  def to_s
+    a = Array::new
+    y = 1
+    while (y <= 9)
+      a.push(sprintf("P%d", y))
+      x = 9
+      while (x >= 1)
+        piece = @array[x][y]
+        if (piece)
+          s = piece.to_s
+        else
+          s = " * "
+        end
+        a.push(s)
+        x = x - 1
+      end
+      a.push(sprintf("\n"))
+      y = y + 1
+    end
+    if (! sente_hands.empty?)
+      a.push("P+")
+      sente_hands.each do |p|
+        a.push("00" + p.name)
+      end
+      a.push("\n")
+    end
+    if (! gote_hands.empty?)
+      a.push("P-")
+      gote_hands.each do |p|
+        a.push("00" + p.name)
+      end
+      a.push("\n")
+    end
+    a.push("%s\n" % [@teban ? "+" : "-"])
+    return a.join
   end
   end
-  def duplicated?(player)
-    if (@hash[player.name])
-      return true
+end
+
+class GameResult
+  attr_reader :players, :black, :white
+
+  def initialize(p1, p2)
+    @players = [p1, p2]
+    if p1.sente && !p2.sente
+      @black, @white = p1, p2
+    elsif !p1.sente && p2.sente
+      @black, @white = p2, p1
     else
     else
-      return false
+      raise "Never reached!"
     end
   end
 end
 
     end
   end
 end
 
+class GameResultWin < GameResult
+  attr_reader :winner, :loser
+
+  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
+    "%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
+  @@mutex = Mutex.new
+  @@time  = 0
 
 
+  def initialize(game_name, player0, player1)
+    @monitors = Array::new
+    @game_name = game_name
+    if (@game_name =~ /-(\d+)-(\d+)$/)
+      @total_time = $1.to_i
+      @byoyomi = $2.to_i
+    end
 
 
-class Player
-  def initialize(str, socket)
-    @name = nil
-    @password = nil
-    @socket = socket
-    @state = "connected"        # wait_game -> game
+    if (player0.sente)
+      @sente, @gote = player0, player1
+    else
+      @sente, @gote = player1, player0
+    end
+    @sente.socket_buffer.clear
+    @gote.socket_buffer.clear
+    @current_player, @next_player = @sente, @gote
+    @sente.game = self
+    @gote.game  = self
 
 
-    @x1 = false                 # extention protocol
-    @eol = "\m"                 # favorite eol code
-    @game = nil
-    @mytime = Total_Time
-    @sente = nil
-    @watchdog_thread = nil
+    @last_move = ""
+    @current_turn = 0
 
 
-    login(str)
+    @sente.status = "agree_waiting"
+    @gote.status  = "agree_waiting"
+
+    @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")
+
+    LEAGUE.games[@id] = self
+
+    log_message(sprintf("game created %s", @id))
+
+    @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
 
   end
 
-  attr_accessor :name, :password, :socket, :state
-  attr_accessor :x1, :eol, :game, :mytime, :watchdog_thread
+  def turn?(player)
+    return player.status == "game" && @current_player == player
+  end
 
 
+  def monitoron(monitor)
+    @monitors.delete(monitor)
+    @monitors.push(monitor)
+  end
 
 
-  def write_safe(str)
-    @socket.write_safe(str + @eol)
+  def monitoroff(monitor)
+    @monitors.delete(monitor)
+  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))
+    finish
+  end
+
+  def kill(killer)
+    if ["agree_waiting", "start_waiting"].include?(@sente.status)
+      reject(killer.name)
+    elsif (@current_player == killer)
+      abnormal_lose()
+      finish
+    end
   end
 
   end
 
-  def login(str)
-    str =~ /([\r\n]*)$/
-    @eol = $1
-    str.chomp!
-    (login, @name, @password, ext) = str.split
-    @x1 = true if (ext)
+  def finish
+    log_message(sprintf("game finished %s", @id))
+    @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
+    @fh.close
+
+    @sente.game = nil
+    @gote.game = nil
+    @sente.status = "connected"
+    @gote.status = "connected"
+
+    if (@current_player.protocol == LoginCSA::PROTOCOL)
+      @current_player.finish
+    end
+    if (@next_player.protocol == LoginCSA::PROTOCOL)
+      @next_player.finish
+    end
+    @monitors = Array::new
+    @sente = nil
+    @gote = nil
+    @current_player = nil
+    @next_player = nil
+    LEAGUE.games.delete(@id)
   end
 
   end
 
-  def run
-    if (@x1)
-      log_message(sprintf("user %s run in x1 mode", @name))
-      write_safe("## LOGIN in x1 mode")
+  # 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
+    @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
     else
-      log_message(sprintf("user %s run in CSA mode", @name))
-    end
-
-    while (str = @socket.gets_safe)
-      str.chomp!
-      case str
-      when /^%%HELP/
-        write_help
-      when /^%%GAME\s+(\S+)\s+([\+\-])/
-        game_name = $1
-        @state = "game_waiting"
-        if ($2 == "+")
-          @sente = true
-          rival_sente = false
-        else
-          @sente = false
-          rival_sente = true
-        end
-        rival = LEAGUE.get_player(game_name, rival_sente)
-        if (rival)
-          @state = "game"
-          LEAGUE.start_game(game_name, self, rival)
-        end
-      when /^%%CHAT\s+(\S+)/
-        message = $1
-        LEAGUE.hash.each do |name, player|
-          s = player.write_safe(sprintf("## [%s] %s", @name, message))
-          player.status = "zombie" if (! s)
+      @current_player.mytime -= t
+      if (@current_player.mytime < 0)
+        @current_player.mytime = 0
+      end
+
+      move_status = @board.handle_one_move(str, @sente == @current_player)
+
+      if [:illegal, :uchifuzme, :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
         end
-      when /^%%WHO/
-        LEAGUE.hash.each do |name, player|
-          write_safe(sprintf("## %s %s", name, player.state))
+
+        @monitors.each do |monitor|
+          monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
+          monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
         end
         end
-      when /^%%LOGOUT/
-        break
-      else
-        write_safe(sprintf("## unknown command %s", str))
       end
     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
   end
   end
-end
 
 
-class Board
-end
+  def abnormal_win
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
+    @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
+    @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
+  end
 
 
-class Game
-  def initialize(event, sente, gote)
-    @id = sprintf("%s-%s-%s-%s", event, sente.name, gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
-    @logfile = @id + ".csa"
-    @sente = sente
-    @gote = gote
-    @sente.sg_flag = "+"
-    @gote.sg_flag = "-"
-    @board = Board::new
-    @currnet_player = sente
-    @next_player = gote
-    @fh = nil
-    printf("%s: new game %s %s %s\n", Time::new.to_s, @id, @sente.name, @gote.name)
+  def abnormal_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
+    @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
+    @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
+  end
+
+  def sennichite_draw
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#SENNICHITE\n#DRAW\n")
+    @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
+  end
+
+  def oute_sennichite_win_lose(winner, loser)
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    loser.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
+    winner.write_safe("#OUTE_SENNICHITE\n#WIN\n")
+    @fh.print(@board.to_s.gsub(/^/, "\'"))
+    if loser == @current_player
+      @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
+    else
+      @fh.printf("'summary:oute_sennichite:%s win:%s lose\n", @current_player.name, @next_player.name)
+    end
+    @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))
+    end
+  end
+
+  def illegal_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
+    @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
+  end
+
+  def uchifuzume_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
+    @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
+  end
+
+  def oute_kaihimore_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
+    @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
+  end
+
+  def timeout_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#TIME_UP\n#LOSE\n")
+    @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
+  end
+
+  def kachi_win
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
+    @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
+    @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
+  end
+
+  def kachi_lose
+    @current_player.status = "connected"
+    @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")
+    @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
+  end
+
+  def toryo_lose
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
+    @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
+    @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
   end
   end
+
+  def outori_win
+    @current_player.status = "connected"
+    @next_player.status = "connected"
+    @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
+    @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
+  end
+
   def start
   def start
-    begin
-      @sente.watchdog_start(Watchdog_Time)
-      @gote.watchdog_start(Watchdog_Time)
-
-      @fh = open(@logfile, "w")
-      @fh.sync = true
-
-      @fh.printf("V2\n")
-      @fh.printf("N+%s\n", @sente.name)
-      @fh.printf("N-%s\n", @gote.name)
-      @fh.printf("$EVENT:%s\n", @id)
-      @sente.write(start_message("+"))
-      @gote.write(start_message("-"))
-      @sente.wait_agree(Agree_Time)
-      @gote.wait_agree(Agree_Time)
-
-      @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
-      @fh.print <<EOM
+    log_message(sprintf("game started %s", @id))
+    @sente.write_safe(sprintf("START:%s\n", @id))
+    @gote.write_safe(sprintf("START:%s\n", @id))
+    @sente.mytime = @total_time
+    @gote.mytime = @total_time
+    @start_time = Time::new
+  end
+
+  def propose
+    @fh = open(@logfile, "w")
+    @fh.sync = true
+
+    @fh.puts("V2")
+    @fh.puts("N+#{@sente.name}")
+    @fh.puts("N-#{@gote.name}")
+    @fh.puts("$EVENT:#{@id}")
+
+    @sente.write_safe(propose_message("+"))
+    @gote.write_safe(propose_message("-"))
+
+    now = Time::new.strftime("%Y/%m/%d %H:%M:%S")
+    @fh.puts("$START_TIME:#{now}")
+    @fh.print <<EOM
 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
-P2 * -HI *  *  *  *  * -KA *
+P2 * -HI *  *  *  *  * -KA * 
 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
-P4 *  *  *  *  *  *  *  *  *
-P5 *  *  *  *  *  *  *  *  *
-P6 *  *  *  *  *  *  *  *  *
+P4 *  *  *  *  *  *  *  *  * 
+P5 *  *  *  *  *  *  *  *  * 
+P6 *  *  *  *  *  *  *  *  * 
 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
-P8 * +KA *  *  *  *  * +HI *
+P8 * +KA *  *  *  *  * +HI * 
 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
 +
 EOM
 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
 +
 EOM
-
-      @sente.write(sprintf("START:%s\n", @id))
-      @gote.write(sprintf("START:%s\n", @id))
-      while(true)
-        @currnet_player = @sente
-        @next_player = @gote
-        handle_one_move(@currnet_player, @next_player)
-
-        @currnet_player = @gote
-        @next_player = @sente
-        handle_one_move(@currnet_player, @next_player)
-      end
-    rescue ShogiWatchdogTimeout
-      sg_flag_of_timeout = $!.message
-      if (sg_flag_of_timeout == "+")
-        loser = @sente
-        winner = @gote
-      else
-        loser = @sente
-        winner = @gote
-      end
-      printf("watchdog timeout by %s\n", loser.name)
-      loser.write("#TIME_UP\n#LOSE\n")
-      winner.write("#TIME_UP\n#WIN\n")
-    rescue TimeoutError, ShogiTimeout
-      printf("%s: end timeup by %s\n", Time::new.to_s, @currnet_player.name)
-      @currnet_player.write("#TIME_UP\n#LOSE\n")
-      @next_player.write("#TIME_UP\n#WIN\n")
-    rescue ShogiReject
-      sender = $!.message
-      printf("%s: reject by %s\n", Time::new.to_s, sender)
-      str = sprintf("REJECT:%s by %s\n", @id, sender)
-      @sente.write(str)
-      @gote.write(str)
-    rescue ShogiIllegalMove
-      printf("%s: end illegal move by %s\n", Time::new.to_s, @currnet_player.name)
-      move = $!.message
-      @fh.printf("%%ERROR\n")
-      @currnet_player.write(sprintf("%s\n#ILLEGAL_MOVE\n#LOSE\n", move))
-      @next_player.write(sprintf("%s\n#ILLEGAL_MOVE\n#WIN\n", move))
-    rescue ShogiEnd
-      printf("%s: end by %s\n", Time::new.to_s, @currnet_player.name)
-      move = $!.message
-      case move
-      when "%TORYO"
-        @currnet_player.write(sprintf("%s\n#RESIGN\n#LOSE\n", move))
-        @next_player.write(sprintf("%s\n#RESIGN\n#WIN\n", move))
-      when "%KACHI"
-        @currnet_player.write(sprintf("%s\n#JISHOGI\n#WIN\n", move))
-        @next_player.write(sprintf("%s\n#JISHOGI\n#LOSE\n", move))
-      end
-    end
-    @fh.printf("'END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
-  end
-
-  def handle_one_move(current_player, next_player)
-    start_time = Time::new
-    str = current_player.get_move
-    @fh.printf("%s\n", str)
-    end_time = Time::new
-    time = (end_time - start_time).truncate
-    time = Least_Time_Per_Move if (time < Least_Time_Per_Move)
-    current_player.sub_time(time)
-    raise ShogiEnd, str if (str =~ /\A%/)
-    @sente.write(sprintf("%s,T%d\n", str, time))
-    @gote.write(sprintf("%s,T%d\n", str, time))
-    @fh.printf("T%s\n", time)
   end
 
   end
 
-  def finish
-    @sente.finish
-    @gote.finish
-    @fh.close
-    printf("%s: end game %s %s %s\n", Time::new.to_s, @id, @sente.name, @gote.name)
+  def show()
+    str0 = <<EOM
+BEGIN Game_Summary
+Protocol_Version:1.1
+Protocol_Mode:Server
+Format:Shogi 1.0
+Declaration:Jishogi 1.1
+Game_ID:#{@id}
+Name+:#{@sente.name}
+Name-:#{@gote.name}
+Rematch_On_Draw:NO
+To_Move:+
+BEGIN Time
+Time_Unit:1sec
+Total_Time:#{@total_time}
+Byoyomi:#{@byoyomi}
+Least_Time_Per_Move:#{Least_Time_Per_Move}
+Remaining_Time+:#{@sente.mytime}
+Remaining_Time-:#{@gote.mytime}
+Last_Move:#{@last_move}
+Current_Turn:#{@current_turn}
+END Time
+BEGIN Position
+EOM
+
+    str1 = <<EOM
+END Position
+END Game_Summary
+EOM
+
+    return str0 + @board.to_s + str1
   end
 
   end
 
-  def start_message(sg_flag)
+  def propose_message(sg_flag)
     str = <<EOM
     str = <<EOM
+BEGIN Game_Summary
+Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
 Protocol_Mode:Server
 Format:Shogi 1.0
+Declaration:Jishogi 1.1
 Game_ID:#{@id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
 Game_ID:#{@id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
@@ -307,27 +2038,46 @@ Rematch_On_Draw:NO
 To_Move:+
 BEGIN Time
 Time_Unit:1sec
 To_Move:+
 BEGIN Time
 Time_Unit:1sec
-Total_Time:#{Total_Time}
+Total_Time:#{@total_time}
+Byoyomi:#{@byoyomi}
 Least_Time_Per_Move:#{Least_Time_Per_Move}
 END Time
 BEGIN Position
 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
 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
-P2 * -HI *  *  *  *  * -KA *
+P2 * -HI *  *  *  *  * -KA * 
 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
-P4 *  *  *  *  *  *  *  *  *
-P5 *  *  *  *  *  *  *  *  *
-P6 *  *  *  *  *  *  *  *  *
+P4 *  *  *  *  *  *  *  *  * 
+P5 *  *  *  *  *  *  *  *  * 
+P6 *  *  *  *  *  *  *  *  * 
 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
-P8 * +KA *  *  *  *  * +HI *
+P8 * +KA *  *  *  *  * +HI * 
 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
 P+
 P-
 +
 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
 P+
 P-
 +
+END Position
+END Game_Summary
 EOM
     return str
   end
 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
+end # module ShogiServer
+
+#################################################
+# MAIN
+#
 
 def usage
     print <<EOM
 
 def usage
     print <<EOM
@@ -335,7 +2085,7 @@ NAME
        shogi-server - server for CSA server protocol
 
 SYNOPSIS
        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
 
 DESCRIPTION
        server for CSA server protocol
@@ -343,6 +2093,8 @@ DESCRIPTION
 OPTIONS
        --pid-file file
                specify filename for logging process ID
 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
 
 LICENSE
        this file is distributed under GPL version2 and might be compiled by Exerb
@@ -350,35 +2102,39 @@ LICENSE
 SEE ALSO
 
 RELEASE
 SEE ALSO
 
 RELEASE
-       #{Release}
+       #{ShogiServer::Release}
 
 REVISION
 
 REVISION
-       #{Revision}
+       #{ShogiServer::Revision}
 EOM
 end
 
 EOM
 end
 
+def log_debug(str)
+  $logger.debug(str)
+end
+
 def log_message(str)
 def log_message(str)
-  printf("%s message: %s\n", Time::new.to_s, str)
+  $logger.info(str)
 end
 
 def log_warning(str)
 end
 
 def log_warning(str)
-  printf("%s message: %s\n", Time::new.to_s, str)
+  $logger.warn(str)
 end
 
 def log_error(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
 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|
   begin
     parser.each_option do |name, arg|
+      name.sub!(/^--/, '')
       options[name] = arg.dup
     end
   rescue
       options[name] = arg.dup
     end
   rescue
@@ -388,60 +2144,149 @@ def parse_command_line
   return options
 end
 
   return options
 end
 
-LEAGUE = League::new
+def write_pid_file(file)
+  open(file, "w") do |fh|
+    fh.puts "#{$$}"
+  end
+end
 
 
-def good_login?(str)
-  return false if (str !~ /^LOGIN /)
-  tokens = str.split
-  if ((tokens.length == 3) || (tokens.length == 4))
-    ## ok
-  else
-    return false
+def mutex_watchdog(mutex, sec)
+  while true
+    begin
+      timeout(sec) do
+        begin
+          mutex.lock
+        ensure
+          mutex.unlock
+        end
+      end
+      sleep(sec)
+    rescue TimeoutError
+      log_error("mutex watchdog timeout")
+      exit(1)
+    end
   end
   end
-  return true
+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
 end
 
 def main
+
+  $mutex = Mutex::new
+  Thread::start do
+    Thread.pass
+    mutex_watchdog($mutex, 10)
+  end
+
   $options = parse_command_line
   if (ARGV.length != 2)
     usage
     exit 2
   end
   $options = parse_command_line
   if (ARGV.length != 2)
     usage
     exit 2
   end
-  event = ARGV.shift
+
+  LEAGUE.event = ARGV.shift
   port = ARGV.shift
 
   port = ARGV.shift
 
-  Thread.abort_on_exception = true
+  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) # thread safe
 
 
-  server = TCPserver.open(port)
-  log_message("server started")
+  LEAGUE.dir = dir || File.dirname(__FILE__)
+  LEAGUE.setup_players_database
 
 
-  while true
-    Thread::start(server.accept) do |client|
-      client.sync = true
-      while (str = client.gets_timeout(Login_Time))
-        Thread::kill(Thread::current) if (! str) # disconnected
-        str =~ /([\r\n]*)$/
-        eol = $1
-        if (good_login?(str))
-          player = Player::new(str, client)
-          if (LEAGUE.duplicated?(player))
-            client.write_safe(sprintf("username %s is already connected", player.name))
-            next
-          end
-          LEAGUE.add(player)
-          break
-        else
-          client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol)
-        end
-      end                       # login loop
-      log_message(sprintf("user %s login", player.name))
-      player.run
-      LEAGUE.delete(player)
-      log_message(sprintf("user %s logout", player.name))
+  config = {}
+  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| 
+    trap(signal) do
+      LEAGUE.shutdown
+      server.shutdown
     end
   end
     end
   end
+  $stderr.puts("server started as a deamon [Revision: #{ShogiServer::Revision}]") if $options["daemon"] 
+  log_message("server started [Revision: #{ShogiServer::Revision}]")
+
+  server.start do |client|
+      # 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, login = login_loop(client) # loop
+      next unless player
+
+      log_message(sprintf("user %s login", player.name))
+      login.process
+      player.run(login.csa_1st_str) # loop
+      begin
+        $mutex.lock
+        if (player.game)
+          player.game.kill(player)
+        end
+        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__)
 if ($0 == __FILE__)
+  STDOUT.sync = true
+  STDERR.sync = true
+  TCPSocket.do_not_reverse_lookup = true
+  Thread.abort_on_exception = $DEBUG ? true : false
+
+  LEAGUE = ShogiServer::League::new
   main
 end
   main
 end