OSDN Git Service

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