OSDN Git Service

Added a new method, Player#wait_write_thread_finish, to have a writer thread finish...
[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 'shogi_server/command'
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     @rating_group = nil
34     @modified_at = nil
35     @sente = nil
36     @game_name = ""
37   end
38
39   # Idetifier of the player in the rating system
40   attr_accessor :player_id
41
42   # Name of the player
43   attr_accessor :name
44   
45   # Password of the player, which does not include a trip
46   attr_accessor :password
47
48   # Score in the rating sysem
49   attr_accessor :rate
50
51   # Number of games for win and loss in the rating system
52   attr_accessor :win, :loss
53   
54   # Group in the rating system
55   attr_accessor :rating_group
56
57   # Last timestamp when the rate was modified
58   attr_accessor :modified_at
59
60   # Whether win the previous game or not
61   attr_accessor :last_game_win
62
63   # true for Sente; false for Gote
64   attr_accessor :sente
65
66   # game name
67   attr_accessor :game_name
68
69   def is_human?
70     return [%r!_human$!, %r!_human@!].any? do |re|
71       re.match(@name)
72     end
73   end
74
75   def is_computer?
76     return !is_human?
77   end
78
79   def modified_at
80     @modified_at || Time.now
81   end
82
83   def rate=(new_rate)
84     if @rate != new_rate
85       @rate = new_rate
86       @modified_at = Time.now
87     end
88   end
89
90   def rated?
91     @player_id != nil
92   end
93
94   def last_game_win?
95     return @last_game_win
96   end
97
98   def simple_player_id
99     if @trip
100       simple_name = @name.gsub(/@.*?$/, '')
101       "%s+%s" % [simple_name, @trip[0..8]]
102     else
103       @name
104     end
105   end
106
107   ##
108   # Parses str in the LOGIN command, sets up @player_id and @trip
109   #
110   def set_password(str)
111     if str && !str.empty?
112       @password = str.strip
113       @player_id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
114     else
115       @player_id = @password = nil
116     end
117   end
118
119   def set_sente_from_str(str)
120     case str
121     when "+" then @sente = true
122     when "-" then @sente = false
123     else
124       # str should be "*"
125       @sente = nil
126     end
127   end
128 end
129
130
131 class Player < BasicPlayer
132   WRITE_THREAD_WATCH_INTERVAL = 20 # sec
133   def initialize(str, socket, eol=nil)
134     super()
135     @socket = socket
136     @status = "connected"       # game_waiting -> agree_waiting -> start_waiting -> game -> finished
137
138     @protocol = nil             # CSA or x1
139     @eol = eol || "\m"          # favorite eol code
140     @game = nil
141     @mytime = 0                 # set in start method also
142     @socket_buffer = []
143     @main_thread = Thread::current
144     @write_queue = ShogiServer::TimeoutQueue.new(WRITE_THREAD_WATCH_INTERVAL)
145     @player_logger = nil
146     start_write_thread
147   end
148
149   attr_accessor :socket, :status
150   attr_accessor :protocol, :eol, :game, :mytime
151   attr_accessor :main_thread
152   attr_reader :socket_buffer
153   
154   def setup_logger(dir)
155     log_file = File.join(dir, "%s.log" % [simple_player_id])
156     @player_logger = Logger.new(log_file, 'daily')
157     @player_logger.formatter = ShogiServer::Formatter.new
158     @player_logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO  
159     @player_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
160   end
161
162   def log(level, direction, message)
163     return unless @player_logger
164     str = message.chomp
165     case direction
166       when :in
167         str = "IN: %s" % [str]
168       when :out
169         str = "OUT: %s" % [str]
170       else
171         str = "UNKNOWN DIRECTION: %s %s" % [direction, str]
172     end
173     case level
174       when :debug
175         @player_logger.debug(str)
176       when :info
177         @player_logger.info(str)
178       when :warn
179         @player_logger.warn(str)
180       when :error
181         @player_logger.error(str)
182       else
183         @player_logger.debug("UNKNOWN LEVEL: %s %s" % [level, str])
184     end
185   rescue Exception => ex
186     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
187   end
188
189   def kill
190     log_message(sprintf("user %s killed", @name))
191     if (@game)
192       @game.kill(self)
193     end
194     finish
195     Thread::kill(@main_thread)  if @main_thread
196     Thread::kill(@write_thread) if @write_thread
197   end
198
199   def finish
200     if (@status != "finished")
201       @status = "finished"
202       log_message(sprintf("user %s finish", @name))    
203       begin
204         log_debug("Terminating %s's write thread..." % [@name])
205         if @write_thread && @write_thread.alive?
206           write_safe(nil)
207           Thread.pass # help the write_thread to terminate
208         end
209         log_debug("done.")
210       rescue
211         log_message(sprintf("user %s finish failed", @name))    
212       end
213     end
214   end
215
216   def start_write_thread
217     @write_thread = Thread.start do
218       Thread.pass
219       while !@socket.closed?
220         begin
221           str = @write_queue.deq
222           if (str == nil)
223             log_debug("%s's write thread terminated" % [@name])
224             break
225           end
226           if (str == :timeout)
227             log_debug("%s's write queue timed out. Try again..." % [@name])
228             next
229           end
230
231           if r = select(nil, [@socket], nil, 20)
232             r[1].first.write(str)
233             log(:info, :out, str)
234           else
235             log_error("Gave a try to send a message to #{@name}, but it timed out.")
236             break
237           end
238         rescue Exception => ex
239           log_error("Failed to send a message to #{@name}. #{ex.class}: #{ex.message}\t#{ex.backtrace[0]}")
240           break
241         end
242       end # while loop
243       log_error("%s's socket closed." % [@name]) if @socket.closed?
244       log_message("At least %d messages are not sent to the client." % 
245                   [@write_queue.get_messages.size])
246     end # thread
247   end
248
249   #
250   # Wait for the write thread to finish.
251   # This method should be called just before this instance will be freed.
252   #
253   def wait_write_thread_finish(msec=1000)
254     while msec > 0 && @write_thread && @write_thread.alive?
255       sleep 0.1; msec -= 0.1
256     end
257     @player_logger.close if @player_logger
258   end
259
260   #
261   # Note that sending a message is included in the giant lock.
262   #
263   def write_safe(str)
264     @write_queue.enq(str)
265   end
266
267   def to_s
268     if ["game_waiting", "start_waiting", "agree_waiting", "game"].include?(status)
269       if (@sente)
270         return sprintf("%s %s %s %s +", rated? ? @player_id : @name, @protocol, @status, @game_name)
271       elsif (@sente == false)
272         return sprintf("%s %s %s %s -", rated? ? @player_id : @name, @protocol, @status, @game_name)
273       elsif (@sente == nil)
274         return sprintf("%s %s %s %s *", rated? ? @player_id : @name, @protocol, @status, @game_name)
275       end
276     else
277       return sprintf("%s %s %s", rated? ? @player_id : @name, @protocol, @status)
278     end
279   end
280
281   def run(csa_1st_str=nil)
282     while ( csa_1st_str || 
283             str = gets_safe(@socket, (@socket_buffer.empty? ? Default_Timeout : 1)) )
284       time = Time.now
285       log(:info, :in, str) if str && str.instance_of?(String) 
286       $mutex.lock
287       begin
288         if !@write_thread.alive?
289           log_error("%s's write thread is dead. Aborting..." % [@name])
290           return
291         end
292         if (@game && @game.turn?(self))
293           @socket_buffer << str
294           str = @socket_buffer.shift
295         end
296         log_debug("%s (%s)" % [str, @socket_buffer.map {|a| String === a ? a.strip : a }.join(",")])
297
298         if (csa_1st_str)
299           str = csa_1st_str
300           csa_1st_str = nil
301         end
302
303         if (@status == "finished")
304           return
305         end
306         str.chomp! if (str.class == String) # may be strip! ?
307
308         delay = Time.now - time
309         if delay > 5
310           log_warning("Detected a long delay: %.2f sec" % [delay])
311         end
312         cmd = ShogiServer::Command.factory(str, self, time)
313         case cmd.call
314         when :return
315           return
316         when :continue
317           # do nothing
318         else
319           # TODO never reach
320         end
321
322       ensure
323         $mutex.unlock
324       end
325     end # enf of while
326     log_warning("%s's socket was suddenly closed" % [@name])
327   end # def run
328 end # class
329
330 end # ShogiServer