OSDN Git Service

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