OSDN Git Service

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