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
33 # Idetifier of the player in the rating system
34 attr_accessor :player_id
39 # Password of the player, which does not include a trip
40 attr_accessor :password
42 # Score in the rating sysem
45 # Number of games for win and loss in the rating system
46 attr_accessor :win, :loss
48 # Group in the rating system
49 attr_accessor :rating_group
51 # Last timestamp when the rate was modified
52 attr_accessor :modified_at
54 # Whether win the previous game or not
55 attr_accessor :last_game_win
58 @modified_at || Time.now
64 @modified_at = Time.now
78 simple_name = @name.gsub(/@.*?$/, '')
79 "%s+%s" % [simple_name, @trip[0..8]]
86 # Parses str in the LOGIN command, sets up @player_id and @trip
91 @player_id = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
93 @player_id = @password = nil
99 class Player < BasicPlayer
100 def initialize(str, socket, eol=nil)
103 @status = "connected" # game_waiting -> agree_waiting -> start_waiting -> game -> finished
105 @protocol = nil # CSA or x1
106 @eol = eol || "\m" # favorite eol code
109 @mytime = 0 # set in start method also
112 @main_thread = Thread::current
113 @mutex_write_guard = Mutex.new
116 attr_accessor :socket, :status
117 attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
118 attr_accessor :main_thread
119 attr_reader :socket_buffer
122 log_message(sprintf("user %s killed", @name))
127 Thread::kill(@main_thread) if @main_thread
131 if (@status != "finished")
133 log_message(sprintf("user %s finish", @name))
135 # @socket.close if (! @socket.closed?)
137 log_message(sprintf("user %s finish failed", @name))
143 @mutex_write_guard.synchronize do
146 log_warning("%s's socket has been closed." % [@name])
149 if r = select(nil, [@socket], nil, 20)
150 r[1].first.write(str)
152 log_error("Sending a message to #{@name} timed up.")
154 rescue Exception => ex
155 log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
161 if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
163 return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
164 elsif (@sente == false)
165 return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
166 elsif (@sente == nil)
167 return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
170 return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
174 def run(csa_1st_str=nil)
175 while ( csa_1st_str ||
176 str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
179 if (@game && @game.turn?(self))
180 @socket_buffer << str
181 str = @socket_buffer.shift
183 log_message("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")]) if $DEBUG
190 if (@status == "finished")
193 str.chomp! if (str.class == String) # may be strip! ?
196 # Application-level protocol for Keep-Alive
197 # If the server gets LF, it sends back LF.
198 # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
201 if (@status == "game")
202 array_str = str.split(",")
203 move = array_str.shift
204 additional = array_str.shift
205 if /^'(.*)/ =~ additional
206 comment = array_str.unshift("'*#{$1.toeuc}")
208 s = @game.handle_one_move(move, self)
209 @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
210 return if (s && @protocol == LoginCSA::PROTOCOL)
212 when /^%[^%]/, :timeout
213 if (@status == "game")
214 s = @game.handle_one_move(str, self)
215 return if (s && @protocol == LoginCSA::PROTOCOL)
218 log_error("Failed to receive a message from #{@name}.")
221 if (@status == "agree_waiting")
223 return if (@protocol == LoginCSA::PROTOCOL)
225 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
228 if (@status == "agree_waiting")
229 @status = "start_waiting"
230 if ((@game.sente.status == "start_waiting") &&
231 (@game.gote.status == "start_waiting"))
233 @game.sente.status = "game"
234 @game.gote.status = "game"
237 write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
239 when /^%%SHOW\s+(\S+)/
241 if (LEAGUE.games[game_id])
242 write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
244 write_safe("##[SHOW] +OK\n")
245 when /^%%MONITORON\s+(\S+)/
247 if (LEAGUE.games[game_id])
248 LEAGUE.games[game_id].monitoron(self)
249 write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
250 write_safe("##[MONITOR][#{game_id}] +OK\n")
252 when /^%%MONITOROFF\s+(\S+)/
254 if (LEAGUE.games[game_id])
255 LEAGUE.games[game_id].monitoroff(self)
259 %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
261 players = LEAGUE.rated_players
262 players.sort {|a,b| b.rate <=> a.rate}.each do |p|
263 write_safe("##[RATING] %s \t %4d @%s\n" %
264 [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
266 write_safe("##[RATING] +OK\n")
268 write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
269 write_safe("##[VERSION] +OK\n")
271 if ((@status == "connected") || (@status == "game_waiting"))
272 @status = "connected"
275 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
277 when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
281 if (! Login::good_game_name?(game_name))
282 write_safe(sprintf("##[ERROR] bad game name\n"))
284 elsif ((@status == "connected") || (@status == "game_waiting"))
287 write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
292 if (League::Floodgate.game_name?(game_name))
293 if (my_sente_str != "*")
294 write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name))
299 if (my_sente_str == "*")
300 rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
301 elsif (my_sente_str == "+")
302 rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
303 elsif (my_sente_str == "-")
304 rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
307 write_safe(sprintf("##[ERROR] bad game option\n"))
313 @game_name = game_name
314 if ((my_sente_str == "*") && (rival.sente == nil))
322 elsif (rival.sente == true) # rival has higher priority
324 elsif (rival.sente == false)
326 elsif (my_sente_str == "+")
329 elsif (my_sente_str == "-")
335 Game::new(@game_name, self, rival)
336 else # rival not found
337 if (command_name == "GAME")
338 @status = "game_waiting"
339 @game_name = game_name
340 if (my_sente_str == "+")
342 elsif (my_sente_str == "-")
348 write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
349 @status = "connected"
354 when /^%%CHAT\s+(.+)/
356 LEAGUE.players.each do |name, player|
357 if (player.protocol != LoginCSA::PROTOCOL)
358 player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message))
363 LEAGUE.games.each do |id, game|
364 buf.push(sprintf("##[LIST] %s\n", id))
366 buf.push("##[LIST] +OK\n")
370 LEAGUE.players.each do |name, player|
371 buf.push(sprintf("##[WHO] %s\n", player.to_s))
373 buf.push("##[WHO] +OK\n")
376 @status = "connected"
377 write_safe("LOGOUT:completed\n")
380 # This command is only available for CSA's official testing server.
381 # So, this means nothing for this program.
382 write_safe("CHALLENGE ACCEPTED\n")
384 ## ignore null string
386 msg = "##[ERROR] unknown command %s\n" % [str]