OSDN Git Service

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