3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 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 module ShogiServer # for a namespace
30 @last_game_win = false
34 # Idetifier of the player in the rating system
35 attr_accessor :player_id
40 # Password of the player, which does not include a trip
41 attr_accessor :password
43 # Score in the rating sysem
46 # Number of games for win and loss in the rating system
47 attr_accessor :win, :loss
49 # Group in the rating system
50 attr_accessor :rating_group
52 # Last timestamp when the rate was modified
53 attr_accessor :modified_at
55 # Whether win the previous game or not
56 attr_accessor :last_game_win
58 # true for Sente; false for Gote
62 @modified_at || Time.now
68 @modified_at = Time.now
82 simple_name = @name.gsub(/@.*?$/, '')
83 "%s+%s" % [simple_name, @trip[0..8]]
90 # Parses str in the LOGIN command, sets up @player_id and @trip
95 @player_id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
97 @player_id = @password = nil
103 class Player < BasicPlayer
104 WRITE_THREAD_WATCH_INTERVAL = 20 # sec
105 def initialize(str, socket, eol=nil)
108 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
110 @protocol = nil # CSA or x1
111 @eol = eol || "\m" # favorite eol code
114 @mytime = 0 # set in start method also
116 @main_thread = Thread::current
117 @write_queue = ShogiServer::TimeoutQueue.new(WRITE_THREAD_WATCH_INTERVAL)
122 attr_accessor :socket, :status
123 attr_accessor :protocol, :eol, :game, :mytime, :game_name
124 attr_accessor :main_thread
125 attr_reader :socket_buffer
127 def setup_logger(dir)
128 log_file = File.join(dir, "%s.log" % [simple_player_id])
129 @player_logger = Logger.new(log_file, 'daily')
130 @player_logger.formatter = ShogiServer::Formatter.new
131 @player_logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
132 @player_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
135 def log(level, direction, message)
136 return unless @player_logger
140 str = "IN: %s" % [str]
142 str = "OUT: %s" % [str]
144 str = "UNKNOWN DIRECTION: %s %s" % [direction, str]
148 @player_logger.debug(str)
150 @player_logger.info(str)
152 @player_logger.warn(str)
154 @player_logger.error(str)
156 @player_logger.debug("UNKNOWN LEVEL: %s %s" % [level, str])
158 rescue Exception => ex
159 log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
163 log_message(sprintf("user %s killed", @name))
168 Thread::kill(@main_thread) if @main_thread
169 Thread::kill(@write_thread) if @write_thread
173 if (@status != "finished")
175 log_message(sprintf("user %s finish", @name))
177 log_debug("Terminating %s's write thread..." % [@name])
178 if @write_thread && @write_thread.alive?
181 @player_logger.close if @player_logger
184 log_message(sprintf("user %s finish failed", @name))
189 def start_write_thread
190 @write_thread = Thread.start do
192 while !@socket.closed?
194 str = @write_queue.deq
196 log_debug("%s's write thread terminated" % [@name])
200 log_debug("%s's write queue timed out. Try again..." % [@name])
204 if r = select(nil, [@socket], nil, 20)
205 r[1].first.write(str)
206 log(:info, :out, str)
208 log_error("Gave a try to send a message to #{@name}, but it timed out.")
211 rescue Exception => ex
212 log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
216 log_error("%s's socket closed." % [@name]) if @socket.closed?
217 log_message("At least %d messages are not sent to the client." %
218 [@write_queue.get_messages.size])
223 # Note that sending a message is included in the giant lock.
226 @write_queue.enq(str)
230 if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
232 return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
233 elsif (@sente == false)
234 return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
235 elsif (@sente == nil)
236 return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
239 return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
243 def run(csa_1st_str=nil)
244 while ( csa_1st_str ||
245 str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
246 log(:info, :in, str) if str && str.instance_of?(String)
249 if !@write_thread.alive?
250 log_error("%s's write thread is dead. Aborting..." % [@name])
253 if (@game && @game.turn?(self))
254 @socket_buffer << str
255 str = @socket_buffer.shift
257 log_debug("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
264 if (@status == "finished")
267 str.chomp! if (str.class == String) # may be strip! ?
270 # Application-level protocol for Keep-Alive
271 # If the server gets LF, it sends back LF.
272 # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
275 if (@status == "game")
276 array_str = str.split(",")
277 move = array_str.shift
278 additional = array_str.shift
280 if /^'(.*)/ =~ additional
281 comment = array_str.unshift("'*#{$1.toeuc}")
283 s = @game.handle_one_move(move, self)
284 @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
285 return if (s && @protocol == LoginCSA::PROTOCOL)
287 when /^%[^%]/, :timeout
288 if (@status == "game")
289 s = @game.handle_one_move(str, self)
290 return if (s && @protocol == LoginCSA::PROTOCOL)
291 elsif ["agree_waiting", "start_waiting"].include?(@status)
292 if @game.prepared_expire?
293 log_warning("#{@status} lasted too long. This play has been expired.")
294 @game.reject("the Server (timed out)")
295 return if (@protocol == LoginCSA::PROTOCOL)
299 log_error("Failed to receive a message from #{@name}.")
302 if (@status == "agree_waiting")
304 return if (@protocol == LoginCSA::PROTOCOL)
306 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
309 if (@status == "agree_waiting")
310 @status = "start_waiting"
311 if ((@game.sente.status == "start_waiting") &&
312 (@game.gote.status == "start_waiting"))
314 @game.sente.status = "game"
315 @game.gote.status = "game"
318 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
320 when /^%%SHOW\s+(\S+)/
322 if ($league.games[game_id])
323 write_safe($league.games[game_id].show.gsub(/^/, '##[SHOW] '))
325 write_safe("##[SHOW] +OK\n")
326 when /^%%MONITORON\s+(\S+)/
328 if ($league.games[game_id])
329 $league.games[game_id].monitoron(self)
330 write_safe($league.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
331 write_safe("##[MONITOR][#{game_id}] +OK\n")
333 when /^%%MONITOROFF\s+(\S+)/
335 if ($league.games[game_id])
336 $league.games[game_id].monitoroff(self)
340 %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
342 players = $league.rated_players
343 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
344 write_safe("##[RATING] %s \t %4d @%s\n" %
345 [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
347 write_safe("##[RATING] +OK\n")
349 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
350 write_safe("##[VERSION] +OK\n")
352 if ((@status == "connected") || (@status == "game_waiting"))
353 @status = "connected"
356 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
358 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
362 if (! Login::good_game_name?(game_name))
363 write_safe(sprintf("##[ERROR] bad game name\n"))
365 elsif ((@status == "connected") || (@status == "game_waiting"))
368 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
373 if (League::Floodgate.game_name?(game_name))
374 if (my_sente_str != "*")
375 write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name))
380 if (my_sente_str == "*")
381 rival = $league.get_player("game_waiting", game_name, nil, self) # no preference
382 elsif (my_sente_str == "+")
383 rival = $league.get_player("game_waiting", game_name, false, self) # rival must be gote
384 elsif (my_sente_str == "-")
385 rival = $league.get_player("game_waiting", game_name, true, self) # rival must be sente
388 write_safe(sprintf("##[ERROR] bad game option\n"))
394 @game_name = game_name
395 if ((my_sente_str == "*") && (rival.sente == nil))
403 elsif (rival.sente == true) # rival has higher priority
405 elsif (rival.sente == false)
407 elsif (my_sente_str == "+")
410 elsif (my_sente_str == "-")
416 Game::new(@game_name, self, rival)
417 else # rival not found
418 if (command_name == "GAME")
419 @status = "game_waiting"
420 @game_name = game_name
421 if (my_sente_str == "+")
423 elsif (my_sente_str == "-")
429 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
430 @status = "connected"
435 when /^%%CHAT\s+(.+)/
437 $league.players.each do |name, player|
438 if (player.protocol != LoginCSA::PROTOCOL)
439 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
444 $league.games.each do |id, game|
445 buf.push(sprintf("##[LIST] %s\n", id))
447 buf.push("##[LIST] +OK\n")
451 $league.players.each do |name, player|
452 buf.push(sprintf("##[WHO] %s\n", player.to_s))
454 buf.push("##[WHO] +OK\n")
457 @status = "connected"
458 write_safe("LOGOUT:completed\n")
461 # This command is only available for CSA's official testing server.
462 # So, this means nothing for this program.
463 write_safe("CHALLENGE ACCEPTED\n")
465 ## ignore null string
467 msg = "##[ERROR] unknown command %s\n" % [str]