OSDN Git Service

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