3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2012 Daigo Moriwaki (daigo at debian dot org)
6 ## This program is free software; you can redistribute it and/or modify
7 ## it under the terms of the GNU General Public License as published by
8 ## the Free Software Foundation; either version 2 of the License, or
9 ## (at your option) any later version.
11 ## This program is distributed in the hope that it will be useful,
12 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 ## GNU General Public License for more details.
16 ## You should have received a copy of the GNU General Public License
17 ## along with this program; if not, write to the Free Software
18 ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 require 'shogi_server/command'
22 module ShogiServer # for a namespace
33 @last_game_win = false
40 # Idetifier of the player in the rating system
41 attr_accessor :player_id
46 # Password of the player, which does not include a trip
47 attr_accessor :password
49 # Score in the rating sysem
52 # Estimated rate for unrated player (rate == 0)
53 # But this value is not persisted and cleared when player logs off.
54 attr_accessor :estimated_rate
56 # Number of games for win and loss in the rating system
57 attr_accessor :win, :loss
59 # Group in the rating system
60 attr_accessor :rating_group
62 # Last timestamp when the rate was modified
63 attr_accessor :modified_at
65 # Whether win the previous game or not
66 attr_accessor :last_game_win
68 # true for Sente; false for Gote
72 attr_accessor :game_name
75 return [%r!_human$!, %r!_human@!].any? do |re|
85 @modified_at || Time.now
91 @modified_at = Time.now
100 return @last_game_win
105 simple_name = @name.gsub(/@.*?$/, '')
106 "%s+%s" % [simple_name, @trip[0..8]]
113 # Parses str in the LOGIN command, sets up @player_id and @trip
115 def set_password(str)
116 if str && !str.empty?
117 @password = str.strip
118 @player_id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
120 @player_id = @password = nil
124 def set_sente_from_str(str)
126 when "+" then @sente = true
127 when "-" then @sente = false
136 class Player < BasicPlayer
137 WRITE_THREAD_WATCH_INTERVAL = 20 # sec
138 def initialize(str, socket, eol=nil)
141 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
143 @protocol = nil # CSA or x1
144 @eol = eol || "\m" # favorite eol code
146 @mytime = 0 # set in start method also
148 @main_thread = Thread::current
149 @write_queue = ShogiServer::TimeoutQueue.new(WRITE_THREAD_WATCH_INTERVAL)
151 @last_command_at = Time.now
155 attr_accessor :socket, :status
156 attr_accessor :protocol, :eol, :game, :mytime
157 attr_accessor :main_thread
158 attr_reader :socket_buffer
159 # It is updated whenever a player sends a new command
160 attr_accessor :last_command_at
162 def setup_logger(dir)
163 log_file = File.join(dir, "%s.log" % [simple_player_id])
164 @player_logger = Logger.new(log_file, 'daily')
165 @player_logger.formatter = ShogiServer::Formatter.new
166 @player_logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
167 @player_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
170 def log(level, direction, message)
171 return unless @player_logger
175 str = "IN: %s" % [str]
177 str = "OUT: %s" % [str]
179 str = "UNKNOWN DIRECTION: %s %s" % [direction, str]
183 @player_logger.debug(str)
185 @player_logger.info(str)
187 @player_logger.warn(str)
189 @player_logger.error(str)
191 @player_logger.debug("UNKNOWN LEVEL: %s %s" % [level, str])
193 rescue Exception => ex
194 log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
198 log_message(sprintf("user %s killed", @name))
203 Thread::kill(@main_thread) if @main_thread
204 Thread::kill(@write_thread) if @write_thread
208 if (@status != "finished")
210 log_message(sprintf("user %s finish", @name))
212 log_debug("Terminating %s's write thread..." % [@name])
213 if @write_thread && @write_thread.alive?
215 Thread.pass # help the write_thread to terminate
219 log_message(sprintf("user %s finish failed", @name))
224 def start_write_thread
225 @write_thread = Thread.start do
227 while !@socket.closed?
229 str = @write_queue.deq
231 log_debug("%s's write thread terminated" % [@name])
235 log_debug("%s's write queue timed out. Try again..." % [@name])
239 if r = select(nil, [@socket], nil, 20)
240 r[1].first.write(str)
241 log(:info, :out, str)
243 log_error("Gave a try to send a message to #{@name}, but it timed out.")
246 rescue Exception => ex
247 log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
251 log_error("%s's socket closed." % [@name]) if @socket.closed?
252 log_message("At least %d messages are not sent to the client." %
253 [@write_queue.get_messages.size])
258 # Wait for the write thread to finish.
259 # This method should be called just before this instance will be freed.
261 def wait_write_thread_finish(msec=1000)
262 while msec > 0 && @write_thread && @write_thread.alive?
263 sleep 0.1; msec -= 0.1
265 @player_logger.close if @player_logger
269 # Note that sending a message is included in the giant lock.
272 @write_queue.enq(str)
276 if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
278 return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
279 elsif (@sente == false)
280 return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
281 elsif (@sente == nil)
282 return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
285 return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
289 def run(csa_1st_str=nil)
290 while ( csa_1st_str ||
291 str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
293 log(:info, :in, str) if str && str.instance_of?(String)
296 if !@write_thread.alive?
297 log_error("%s's write thread is dead. Aborting..." % [@name])
300 if (@game && @game.turn?(self))
301 @socket_buffer << str
302 str = @socket_buffer.shift
304 log_debug("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
311 if (@status == "finished")
314 str.chomp! if (str.class == String) # may be strip! ?
316 delay = Time.now - time
318 log_warning("Detected a long delay: %.2f sec" % [delay])
320 cmd = ShogiServer::Command.factory(str, self, time)
328 log_error("Detected a wrong return value for %s" % [cmd])
335 log_warning("%s's socket was suddenly closed" % [@name])