OSDN Git Service

- Give default values to BasicPlayer, which fixed undefined
[shogi-server/shogi-server.git] / shogi-server
index 00ef4e1..6a77f86 100755 (executable)
@@ -82,7 +82,7 @@ class League
             charge
           rescue Exception => ex 
             # ignore errors
-            log_error("[in Floodgate's thread] #{ex}")
+            log_error("[in Floodgate's thread] #{ex} #{ex.backtrace}")
           end
         end
       end
@@ -121,6 +121,80 @@ class League
     end
   end # class Floodgate
 
+  #
+  # This manages those players who have their player_id.
+  # Since mk_rate mainly updates the yaml file, basically,
+  # this only reads data. But this writes some properties.
+  # TODO Such data should be facoted out to another file
+  #
+  class Persistent
+    def initialize(filename)
+      @db = YAML::Store.new(filename)
+      @db.transaction do |pstore|
+        @db['players'] ||= Hash.new
+      end
+    end
+
+    #
+    # trancaction=true means read only
+    #
+    def each_group(transaction=false)
+      @db.transaction(transaction) do
+        groups = @db["players"] || Hash.new
+        groups.each do |group, players|
+          yield group,players
+        end
+      end
+    end
+
+    def load_player(player)
+      return unless player.player_id
+
+      hash = nil
+      each_group(true) do |group, players|
+        hash = players[player.player_id]
+        break if hash
+      end
+      return unless hash
+
+      # a current user
+      player.name          = hash['name']
+      player.rate          = hash['rate'] || 0
+      player.modified_at   = hash['last_modified']
+      player.rating_group  = hash['rating_group']
+      player.win           = hash['win']  || 0
+      player.loss          = hash['loss'] || 0
+      player.last_game_win = hash['last_game_win'] || false
+    end
+
+    def save(player)
+      return unless player.player_id
+
+      each_group do |group, players|
+        hash = players[player.player_id]
+        if hash
+          # write only this property. 
+          # the others are updated by ./mk_rate
+          hash['last_game_win'] = player.last_game_win
+          break
+        end
+      end
+    end
+
+    def get_players
+      players = []
+      each_group(true) do |group, players_hash|
+        players << players_hash.keys
+      end
+      return players.flatten.collect do |player_id|
+        p = BasicPlayer.new
+        p.player_id = player_id
+        load_player(p)
+        p
+      end
+    end
+  end # class Persistent
+
   def initialize
     @mutex = Mutex.new # guard @players
     @games = Hash::new
@@ -134,33 +208,38 @@ class League
 
   def shutdown
     @mutex.synchronize do
-      @players.each {|a| save(a)}
+      @players.each do |name, player| 
+        @persistent.save(player)
+      end
     end
     @floodgate.shutdown
   end
 
   # this should be called just after instanciating a League object.
   def setup_players_database
-    @db = YAML::Store.new(File.join(@dir, "players.yaml"))
+    filename = File.join(@dir, "players.yaml")
+    @persistent = Persistent.new(filename)
   end
 
   def add(player)
-    self.load(player) if player.id
+    @persistent.load_player(player)
     @mutex.synchronize do
       @players[player.name] = player
     end
   end
   
   def delete(player)
+    @persistent.save(player)
     @mutex.synchronize do
-      save(player)
       @players.delete(player.name)
     end
   end
 
   def reload
     @mutex.synchronize do
-      @players.each {|player| load(player)}
+      @players.each do |name, player| 
+        @persistent.load_player(player)
+      end
     end
   end
 
@@ -197,59 +276,8 @@ class League
     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
+    return @persistent.get_players
   end
 end
 
@@ -377,14 +405,17 @@ end
 
 class BasicPlayer
   def initialize
-    @id = nil
+    @player_id = nil
     @name = nil
     @password = nil
+    @rate = 0
+    @win  = 0
+    @loss = 0
     @last_game_win = false
   end
 
   # Idetifier of the player in the rating system
-  attr_accessor :id
+  attr_accessor :player_id
 
   # Name of the player
   attr_accessor :name
@@ -419,14 +450,14 @@ class BasicPlayer
   end
 
   def rated?
-    @id != nil
+    @player_id != nil
   end
 
   def last_game_win?
     return @last_game_win
   end
 
-  def simple_id
+  def simple_player_id
     if @trip
       simple_name = @name.gsub(/@.*?$/, '')
       "%s+%s" % [simple_name, @trip[0..8]]
@@ -436,14 +467,14 @@ class BasicPlayer
   end
 
   ##
-  # Parses str in the LOGIN command, sets up @id and @trip
+  # Parses str in the LOGIN command, sets up @player_id and @trip
   #
   def set_password(str)
     if str && !str.empty?
       @password = str.strip
-      @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
+      @player_id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
     else
-      @id = @password = nil
+      @player_id = @password = nil
     end
   end
 end
@@ -461,12 +492,15 @@ class Player < BasicPlayer
     @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))
@@ -490,12 +524,20 @@ class Player < BasicPlayer
   end
 
   def write_safe(str)
-    begin
-      @socket.write(str)
-    rescue Exception => ex
-      log_error("Failed to send a message to #{@name}.")
-      log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
-      # TODO close
+    @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
 
@@ -514,9 +556,15 @@ class Player < BasicPlayer
   end
 
   def run(csa_1st_str=nil)
-    while (csa_1st_str || (str = gets_safe(@socket, Default_Timeout)))
+    while ( csa_1st_str || 
+            str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
+      $mutex.lock
       begin
-        $mutex.lock
+        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
@@ -527,7 +575,6 @@ class Player < BasicPlayer
           return
         end
         str.chomp! if (str.class == String) # may be strip! ?
-        log_message(str) if $DEBUG
         case str 
         when "" 
           # Application-level protocol for Keep-Alive
@@ -543,7 +590,7 @@ class Player < BasicPlayer
               comment = array_str.unshift("'*#{$1.toeuc}")
             end
             s = @game.handle_one_move(move, self)
-            @game.fh.print("#{comment}\n") if (comment && !s)
+            @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
             return if (s && @protocol == LoginCSA::PROTOCOL)
           end
         when /^%[^%]/, :timeout
@@ -606,7 +653,7 @@ class Player < BasicPlayer
           players = LEAGUE.rated_players
           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
             write_safe("##[RATING] %s \t %4d @%s\n" % 
-                       [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
+                       [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
           end
           write_safe("##[RATING] +OK\n")
         when /^%%VERSION/
@@ -1558,8 +1605,8 @@ class GameResultWin < GameResult
   end
 
   def to_s
-    black_name = @black.id || @black.name
-    white_name = @white.id || @white.name
+    black_name = @black.player_id || @black.name
+    white_name = @white.player_id || @white.name
     "%s:%s" % [black_name, white_name]
   end
 end
@@ -1589,6 +1636,8 @@ class Game
     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
@@ -1599,14 +1648,21 @@ class Game
     @sente.status = "agree_waiting"
     @gote.status  = "agree_waiting"
 
-    @id = sprintf("%s+%s+%s+%s+%s", 
+    @game_id = sprintf("%s+%s+%s+%s+%s", 
                   LEAGUE.event, @game_name, 
                   @sente.name, @gote.name, issue_current_time)
-    @logfile = File.join(LEAGUE.dir, @id + ".csa")
+    
+    now = Time.now
+    log_dir_name = File.join(LEAGUE.dir, 
+                             now.strftime("%Y"),
+                             now.strftime("%m"),
+                             now.strftime("%d"))
+    FileUtils.mkdir_p(log_dir_name) unless File.exist?(log_dir_name)
+    @logfile = File.join(log_dir_name, @game_id + ".csa")
 
-    LEAGUE.games[@id] = self
+    LEAGUE.games[@game_id] = self
 
-    log_message(sprintf("game created %s", @id))
+    log_message(sprintf("game created %s", @game_id))
 
     @board = Board::new
     @board.initial
@@ -1616,7 +1672,7 @@ class Game
 
     propose
   end
-  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
+  attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :game_id, :board, :current_player, :next_player, :fh, :monitors
   attr_accessor :last_move, :current_turn
   attr_reader   :result
 
@@ -1624,6 +1680,10 @@ class Game
     @sente.rated? && @gote.rated?
   end
 
+  def turn?(player)
+    return player.status == "game" && @current_player == player
+  end
+
   def monitoron(monitor)
     @monitors.delete(monitor)
     @monitors.push(monitor)
@@ -1634,8 +1694,8 @@ class Game
   end
 
   def reject(rejector)
-    @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
-    @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
+    @sente.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
+    @gote.write_safe(sprintf("REJECT:%s by %s\n", @game_id, rejector))
     finish
   end
 
@@ -1649,7 +1709,7 @@ class Game
   end
 
   def finish
-    log_message(sprintf("game finished %s", @id))
+    log_message(sprintf("game finished %s", @game_id))
     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
     @fh.close
 
@@ -1669,11 +1729,20 @@ class Game
     @gote = nil
     @current_player = nil
     @next_player = nil
-    LEAGUE.games.delete(@id)
+    LEAGUE.games.delete(@game_id)
   end
 
+  # class Game
   def handle_one_move(str, player)
-    return nil unless @current_player == 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
@@ -1693,10 +1762,11 @@ class Game
 
       move_status = @board.handle_one_move(str, @sente == @current_player)
 
-      if [:illegal, :uchifuzme, :oute_kaihimore].include?(move_status)
+      if [:illegal, :uchifuzume, :oute_kaihimore].include?(move_status)
         @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
       else
         if [:normal, :outori, :sennichite, :oute_sennichite_sente_lose, :oute_sennichite_gote_lose].include?(move_status)
+          # Thinking time includes network traffic
           @sente.write_safe(sprintf("%s,T%d\n", str, t))
           @gote.write_safe(sprintf("%s,T%d\n", str, t))
           @fh.printf("%s\nT%d\n", str, t)
@@ -1705,8 +1775,8 @@ class Game
         end
 
         @monitors.each do |monitor|
-          monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
-          monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
+          monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@game_id}] "))
+          monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @game_id))
         end
       end
     end
@@ -1755,7 +1825,7 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1770,7 +1840,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1784,7 +1854,7 @@ class Game
     @result = GameResultDraw.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @game_id))
     end
   end
 
@@ -1802,7 +1872,7 @@ class Game
     @result = GameResultWin.new(winner, loser)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @game_id))
     end
   end
 
@@ -1816,7 +1886,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1830,7 +1900,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1844,7 +1914,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
@@ -1858,7 +1928,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @game_id))
     end
   end
 
@@ -1873,7 +1943,7 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id))
     end
   end
 
@@ -1888,7 +1958,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @game_id))
     end
   end
 
@@ -1903,7 +1973,7 @@ class Game
     @result = GameResultWin.new(@next_player, @current_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @game_id))
     end
   end
 
@@ -1917,14 +1987,14 @@ class Game
     @result = GameResultWin.new(@current_player, @next_player)
     @fh.printf("'rating:#{@result.to_s}\n") if rated?
     @monitors.each do |monitor|
-      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
+      monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @game_id))
     end
   end
 
   def start
-    log_message(sprintf("game started %s", @id))
-    @sente.write_safe(sprintf("START:%s\n", @id))
-    @gote.write_safe(sprintf("START:%s\n", @id))
+    log_message(sprintf("game started %s", @game_id))
+    @sente.write_safe(sprintf("START:%s\n", @game_id))
+    @gote.write_safe(sprintf("START:%s\n", @game_id))
     @sente.mytime = @total_time
     @gote.mytime = @total_time
     @start_time = Time::new
@@ -1937,7 +2007,7 @@ class Game
     @fh.puts("V2")
     @fh.puts("N+#{@sente.name}")
     @fh.puts("N-#{@gote.name}")
-    @fh.puts("$EVENT:#{@id}")
+    @fh.puts("$EVENT:#{@game_id}")
 
     @sente.write_safe(propose_message("+"))
     @gote.write_safe(propose_message("-"))
@@ -1965,7 +2035,7 @@ Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
 Declaration:Jishogi 1.1
-Game_ID:#{@id}
+Game_ID:#{@game_id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
 Rematch_On_Draw:NO
@@ -1998,7 +2068,7 @@ Protocol_Version:1.1
 Protocol_Mode:Server
 Format:Shogi 1.0
 Declaration:Jishogi 1.1
-Game_ID:#{@id}
+Game_ID:#{@game_id}
 Name+:#{@sente.name}
 Name-:#{@gote.name}
 Your_Turn:#{sg_flag}
@@ -2162,8 +2232,8 @@ def login_loop(client)
         LEAGUE.add(player)
         break
       else
-        client.write_safe("LOGIN:incorrect" + eol)
-        client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
+        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