OSDN Git Service

- If a player, including a monitor, stuck at the time of sending
[shogi-server/shogi-server.git] / shogi_server / player.rb
1 ## $Id$
2
3 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
4 ## Copyright (C) 2007-2008 Daigo Moriwaki (daigo at debian dot org)
5 ##
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.
10 ##
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.
15 ##
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
19
20 module ShogiServer # for a namespace
21
22 class BasicPlayer
23   def initialize
24     @player_id = nil
25     @name = nil
26     @password = nil
27     @rate = 0
28     @win  = 0
29     @loss = 0
30     @last_game_win = false
31   end
32
33   # Idetifier of the player in the rating system
34   attr_accessor :player_id
35
36   # Name of the player
37   attr_accessor :name
38   
39   # Password of the player, which does not include a trip
40   attr_accessor :password
41
42   # Score in the rating sysem
43   attr_accessor :rate
44
45   # Number of games for win and loss in the rating system
46   attr_accessor :win, :loss
47   
48   # Group in the rating system
49   attr_accessor :rating_group
50
51   # Last timestamp when the rate was modified
52   attr_accessor :modified_at
53
54   # Whether win the previous game or not
55   attr_accessor :last_game_win
56
57   def modified_at
58     @modified_at || Time.now
59   end
60
61   def rate=(new_rate)
62     if @rate != new_rate
63       @rate = new_rate
64       @modified_at = Time.now
65     end
66   end
67
68   def rated?
69     @player_id != nil
70   end
71
72   def last_game_win?
73     return @last_game_win
74   end
75
76   def simple_player_id
77     if @trip
78       simple_name = @name.gsub(/@.*?$/, '')
79       "%s+%s" % [simple_name, @trip[0..8]]
80     else
81       @name
82     end
83   end
84
85   ##
86   # Parses str in the LOGIN command, sets up @player_id and @trip
87   #
88   def set_password(str)
89     if str && !str.empty?
90       @password = str.strip
91       @player_id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
92     else
93       @player_id = @password = nil
94     end
95   end
96 end
97
98
99 class Player < BasicPlayer
100   def initialize(str, socket, eol=nil)
101     super()
102     @socket = socket
103     @status = "connected"       # game_waiting -> agree_waiting -> start_waiting -> game -> finished
104
105     @protocol = nil             # CSA or x1
106     @eol = eol || "\m"          # favorite eol code
107     @game = nil
108     @game_name = ""
109     @mytime = 0                 # set in start method also
110     @sente = nil
111     @socket_buffer = []
112     @main_thread = Thread::current
113     @write_queue = Queue.new
114     start_write_thread
115   end
116
117   attr_accessor :socket, :status
118   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
119   attr_accessor :main_thread
120   attr_reader :socket_buffer
121   
122   def kill
123     log_message(sprintf("user %s killed", @name))
124     if (@game)
125       @game.kill(self)
126     end
127     finish
128     Thread::kill(@main_thread)  if @main_thread
129     Thread::kill(@write_thread) if @write_thread
130   end
131
132   def finish
133     if (@status != "finished")
134       @status = "finished"
135       log_message(sprintf("user %s finish", @name))    
136       begin
137 #        @socket.close if (! @socket.closed?)
138         write_safe(nil)
139         @write_thread.join
140       rescue
141         log_message(sprintf("user %s finish failed", @name))    
142       end
143     end
144   end
145
146   def start_write_thread
147     @write_thread = Thread.start do
148       Thread.pass
149       while !@socket.closed?
150         str = ""
151         begin
152           begin
153             timeout(5) do
154               str = @write_queue.deq
155             end
156             if str == nil
157               break
158             end
159           rescue TimeoutError
160             next
161           end
162           if r = select(nil, [@socket], nil, 20)
163             r[1].first.write(str)
164           else
165             log_error("Sending a message to #{@name} timed up.")
166           end
167         rescue Exception => ex
168           log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
169         end
170       end # while loop
171       log_message("terminated %s's write thread" % [@name])
172     end # thread
173   rescue
174
175   end
176
177   #
178   # Note that sending a message is included in the giant lock.
179   #
180   def write_safe(str)
181     @write_queue.enq str
182   end
183
184   def to_s
185     if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
186       if (@sente)
187         return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
188       elsif (@sente == false)
189         return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
190       elsif (@sente == nil)
191         return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
192       end
193     else
194       return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
195     end
196   end
197
198   def run(csa_1st_str=nil)
199     while ( csa_1st_str || 
200             str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
201       $mutex.lock
202       begin
203         if !@write_thread.alive?
204           log_error("%s's write thread is dead. Aborting..." % [@name])
205           return
206         end
207         if (@game && @game.turn?(self))
208           @socket_buffer << str
209           str = @socket_buffer.shift
210         end
211         log_message("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")]) if $DEBUG
212
213         if (csa_1st_str)
214           str = csa_1st_str
215           csa_1st_str = nil
216         end
217
218         if (@status == "finished")
219           return
220         end
221         str.chomp! if (str.class == String) # may be strip! ?
222         case str 
223         when "" 
224           # Application-level protocol for Keep-Alive
225           # If the server gets LF, it sends back LF.
226           # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
227           write_safe("\n")
228         when /^[\+\-][^%]/
229           if (@status == "game")
230             array_str = str.split(",")
231             move = array_str.shift
232             additional = array_str.shift
233             if /^'(.*)/ =~ additional
234               comment = array_str.unshift("'*#{$1.toeuc}")
235             end
236             s = @game.handle_one_move(move, self)
237             @game.fh.print("#{Kconv.toeuc(comment.first)}\n") if (comment && comment.first && !s)
238             return if (s && @protocol == LoginCSA::PROTOCOL)
239           end
240         when /^%[^%]/, :timeout
241           if (@status == "game")
242             s = @game.handle_one_move(str, self)
243             return if (s && @protocol == LoginCSA::PROTOCOL)
244           end
245         when :exception
246           log_error("Failed to receive a message from #{@name}.")
247           return
248         when /^REJECT/
249           if (@status == "agree_waiting")
250             @game.reject(@name)
251             return if (@protocol == LoginCSA::PROTOCOL)
252           else
253             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
254           end
255         when /^AGREE/
256           if (@status == "agree_waiting")
257             @status = "start_waiting"
258             if ((@game.sente.status == "start_waiting") &&
259                 (@game.gote.status == "start_waiting"))
260               @game.start
261               @game.sente.status = "game"
262               @game.gote.status = "game"
263             end
264           else
265             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
266           end
267         when /^%%SHOW\s+(\S+)/
268           game_id = $1
269           if (LEAGUE.games[game_id])
270             write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
271           end
272           write_safe("##[SHOW] +OK\n")
273         when /^%%MONITORON\s+(\S+)/
274           game_id = $1
275           if (LEAGUE.games[game_id])
276             LEAGUE.games[game_id].monitoron(self)
277             write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
278             write_safe("##[MONITOR][#{game_id}] +OK\n")
279           end
280         when /^%%MONITOROFF\s+(\S+)/
281           game_id = $1
282           if (LEAGUE.games[game_id])
283             LEAGUE.games[game_id].monitoroff(self)
284           end
285         when /^%%HELP/
286           write_safe(
287             %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
288         when /^%%RATING/
289           players = LEAGUE.rated_players
290           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
291             write_safe("##[RATING] %s \t %4d @%s\n" % 
292                        [p.simple_player_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
293           end
294           write_safe("##[RATING] +OK\n")
295         when /^%%VERSION/
296           write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
297           write_safe("##[VERSION] +OK\n")
298         when /^%%GAME\s*$/
299           if ((@status == "connected") || (@status == "game_waiting"))
300             @status = "connected"
301             @game_name = ""
302           else
303             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
304           end
305         when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
306           command_name = $1
307           game_name = $2
308           my_sente_str = $3
309           if (! Login::good_game_name?(game_name))
310             write_safe(sprintf("##[ERROR] bad game name\n"))
311             next
312           elsif ((@status == "connected") || (@status == "game_waiting"))
313             ## continue
314           else
315             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
316             next
317           end
318
319           rival = nil
320           if (League::Floodgate.game_name?(game_name))
321             if (my_sente_str != "*")
322               write_safe(sprintf("##[ERROR] You are not allowed to specify TEBAN %s for the game %s\n", my_sente_str, game_name))
323               next
324             end
325             @sente = nil
326           else
327             if (my_sente_str == "*")
328               rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
329             elsif (my_sente_str == "+")
330               rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
331             elsif (my_sente_str == "-")
332               rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
333             else
334               ## never reached
335               write_safe(sprintf("##[ERROR] bad game option\n"))
336               next
337             end
338           end
339
340           if (rival)
341             @game_name = game_name
342             if ((my_sente_str == "*") && (rival.sente == nil))
343               if (rand(2) == 0)
344                 @sente = true
345                 rival.sente = false
346               else
347                 @sente = false
348                 rival.sente = true
349               end
350             elsif (rival.sente == true) # rival has higher priority
351               @sente = false
352             elsif (rival.sente == false)
353               @sente = true
354             elsif (my_sente_str == "+")
355               @sente = true
356               rival.sente = false
357             elsif (my_sente_str == "-")
358               @sente = false
359               rival.sente = true
360             else
361               ## never reached
362             end
363             Game::new(@game_name, self, rival)
364           else # rival not found
365             if (command_name == "GAME")
366               @status = "game_waiting"
367               @game_name = game_name
368               if (my_sente_str == "+")
369                 @sente = true
370               elsif (my_sente_str == "-")
371                 @sente = false
372               else
373                 @sente = nil
374               end
375             else                # challenge
376               write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
377               @status = "connected"
378               @game_name = ""
379               @sente = nil
380             end
381           end
382         when /^%%CHAT\s+(.+)/
383           message = $1
384           LEAGUE.players.each do |name, player|
385             if (player.protocol != LoginCSA::PROTOCOL)
386               player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
387             end
388           end
389         when /^%%LIST/
390           buf = Array::new
391           LEAGUE.games.each do |id, game|
392             buf.push(sprintf("##[LIST] %s\n", id))
393           end
394           buf.push("##[LIST] +OK\n")
395           write_safe(buf.join)
396         when /^%%WHO/
397           buf = Array::new
398           LEAGUE.players.each do |name, player|
399             buf.push(sprintf("##[WHO] %s\n", player.to_s))
400           end
401           buf.push("##[WHO] +OK\n")
402           write_safe(buf.join)
403         when /^LOGOUT/
404           @status = "connected"
405           write_safe("LOGOUT:completed\n")
406           return
407         when /^CHALLENGE/
408           # This command is only available for CSA's official testing server.
409           # So, this means nothing for this program.
410           write_safe("CHALLENGE ACCEPTED\n")
411         when /^\s*$/
412           ## ignore null string
413         else
414           msg = "##[ERROR] unknown command %s\n" % [str]
415           write_safe(msg)
416           log_error(msg)
417         end
418       ensure
419         $mutex.unlock
420       end
421     end # enf of while
422   end # def run
423 end # class
424
425 end # ShogiServer