OSDN Git Service

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