OSDN Git Service

Implemented CHALLENGE command
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## $Id$
3
4 ## Copyright (C) 2004 NABEYA Kenichi (aka nanami@2ch)
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 'getoptlong'
21 require 'thread'
22 require 'timeout'
23 require 'socket'
24 require 'yaml'
25 require 'yaml/store'
26 require 'digest/md5'
27 require 'webrick'
28 require 'fileutils'
29
30
31 class TCPSocket
32   def gets_timeout(t = Default_Timeout)
33     begin
34       timeout(t) do
35         return self.gets
36       end
37     rescue TimeoutError
38       return nil
39     rescue
40       return nil
41     end
42   end
43   def gets_safe(t = nil)
44     if (t && t > 0)
45       begin
46         timeout(t) do
47           return self.gets
48         end
49       rescue TimeoutError
50         return :timeout
51       rescue Exception => ex
52         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
53         return :exception
54       end
55     else
56       begin
57         return self.gets
58       rescue
59         return nil
60       end
61     end
62   end
63   def write_safe(str)
64     begin
65       return self.write(str)
66     rescue
67       return nil
68     end
69   end
70 end
71
72
73 module ShogiServer # for a namespace
74
75 Max_Write_Queue_Size = 1000
76 Max_Identifier_Length = 32
77 Default_Timeout = 60            # for single socket operation
78
79 Default_Game_Name = "default-1500-0"
80
81 One_Time = 10
82 Least_Time_Per_Move = 1
83 Login_Time = 300                # time for LOGIN
84
85 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
86 Release.concat("-") if (Release == "")
87 Revision = "$Revision$".gsub(/[^\.\d]/, '')
88
89
90 class League
91   def initialize
92     @games = Hash::new
93     @players = Hash::new
94     @event = nil
95     @dir = File.dirname(__FILE__)
96   end
97   attr_accessor :players, :games, :event, :dir
98
99   # this should be called just after instanciating a League object.
100   def setup_players_database
101     @db = YAML::Store.new(File.join(@dir, "players.yaml"))
102   end
103
104   def add(player)
105     self.load(player) if player.id
106     @players[player.name] = player
107   end
108   
109   def delete(player)
110     @players.delete(player.name)
111   end
112   
113   def get_player(status, game_name, sente, searcher=nil)
114     @players.each do |name, player|
115       if ((player.status == status) &&
116           (player.game_name == game_name) &&
117           ((sente == nil) || (player.sente == nil) || (player.sente == sente)) &&
118           ((searcher == nil) || (player != searcher)))
119         return player
120       end
121     end
122     return nil
123   end
124   
125   def load(player)
126     hash = search(player.id)
127     if hash
128       # a current user
129       player.name         = hash['name']
130       player.rate         = hash['rate']
131       player.modified_at  = hash['last_modified']
132       player.rating_group = hash['rating_group']
133     end
134   end
135
136   def search(id)
137     hash = nil
138     @db.transaction do
139       break unless  @db["players"]
140       @db["players"].each do |group, players|
141         hash = players[id]
142         break if hash
143       end
144     end
145     hash
146   end
147
148   def rated_players
149     players = []
150     @db.transaction(true) do
151       break unless  @db["players"]
152       @db["players"].each do |group, players_hash|
153         players << players_hash.keys
154       end
155     end
156     return players.flatten.collect do |id|
157       p = BasicPlayer.new
158       p.id = id
159       self.load(p)
160       p
161     end
162   end
163 end
164
165
166 ######################################################
167 # Processes the LOGIN command.
168 #
169 class Login
170   def Login.good_login?(str)
171     tokens = str.split
172     if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
173         (tokens[0] == "LOGIN") &&
174         (good_identifier?(tokens[1])))
175       return true
176     else
177       return false
178     end
179   end
180
181   def Login.good_game_name?(str)
182     if ((str =~ /^(.+)-\d+-\d+$/) && (good_identifier?($1)))
183       return true
184     else
185       return false
186     end
187   end
188
189   def Login.good_identifier?(str)
190     if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
191       return true
192     else
193       return false
194     end
195   end
196
197   def Login.factory(str, player)
198     (login, player.name, password, ext) = str.chomp.split
199     if (ext)
200       return Loginx1.new(player, password)
201     else
202       return LoginCSA.new(player, password)
203     end
204   end
205
206   attr_reader :player
207   
208   # the first command that will be executed just after LOGIN.
209   # If it is nil, the default process will be started.
210   attr_reader :csa_1st_str
211
212   def initialize(player, password)
213     @player = player
214     @csa_1st_str = nil
215     parse_password(password)
216   end
217
218   def process
219     @player.write_safe(sprintf("LOGIN:%s OK\n", @player.name))
220     log_message(sprintf("user %s run in %s mode", @player.name, @player.protocol))
221   end
222
223   def incorrect_duplicated_player(str)
224     @player.write_safe("LOGIN:incorrect\n")
225     @player.write_safe(sprintf("username %s is already connected\n", @player.name)) if (str.split.length >= 4)
226     sleep 3 # wait for sending the above messages.
227     @player.name = "%s [duplicated]" % [@player.name]
228     @player.finish
229   end
230 end
231
232 ######################################################
233 # Processes LOGIN for the CSA standard mode.
234 #
235 class LoginCSA < Login
236   PROTOCOL = "CSA"
237
238   def initialize(player, password)
239     @gamename = nil
240     super
241     @player.protocol = PROTOCOL
242   end
243
244   def parse_password(password)
245     if Login.good_game_name?(password)
246       @gamename = password
247       @player.set_password(nil)
248     elsif password.split(",").size > 1
249       @gamename, *trip = password.split(",")
250       @player.set_password(trip.join(","))
251     else
252       @player.set_password(password)
253       @gamename = Default_Game_Name
254     end
255     @gamename = self.class.good_game_name?(@gamename) ? @gamename : Default_Game_Name
256   end
257
258   def process
259     super
260     @csa_1st_str = "%%GAME #{@gamename} *"
261   end
262 end
263
264 ######################################################
265 # Processes LOGIN for the extented mode.
266 #
267 class Loginx1 < Login
268   PROTOCOL = "x1"
269
270   def initialize(player, password)
271     super
272     @player.protocol = PROTOCOL
273   end
274   
275   def parse_password(password)
276     @player.set_password(password)
277   end
278
279   def process
280     super
281     @player.write_safe(sprintf("##[LOGIN] +OK %s\n", PROTOCOL))
282   end
283 end
284
285
286 class BasicPlayer
287   # Idetifier of the player in the rating system
288   attr_accessor :id
289
290   # Name of the player
291   attr_accessor :name
292   
293   # Password of the player, which does not include a trip
294   attr_accessor :password
295
296   # Score in the rating sysem
297   attr_accessor :rate
298   
299   # Group in the rating system
300   attr_accessor :rating_group
301
302   # Last timestamp when the rate was modified
303   attr_accessor :modified_at
304
305   def initialize
306     @name = nil
307     @password = nil
308   end
309
310   def modified_at
311     @modified_at || Time.now
312   end
313
314   def rate=(new_rate)
315     if @rate != new_rate
316       @rate = new_rate
317       @modified_at = Time.now
318     end
319   end
320
321   def rated?
322     @id != nil
323   end
324
325   def simple_id
326     if @trip
327       simple_name = @name.gsub(/@.*?$/, '')
328       "%s+%s" % [simple_name, @trip[0..8]]
329     else
330       @name
331     end
332   end
333
334   ##
335   # Parses str in the LOGIN command, sets up @id and @trip
336   #
337   def set_password(str)
338     if str && !str.empty?
339       @password = str.strip
340       @id   = "%s+%s" % [@name, Digest::MD5.hexdigest(@password)]
341     else
342       @id = @password = nil
343     end
344   end
345 end
346
347
348 class Player < BasicPlayer
349   def initialize(str, socket)
350     super()
351     @socket = socket
352     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
353
354     @protocol = nil             # CSA or x1
355     @eol = "\m"                 # favorite eol code
356     @game = nil
357     @game_name = ""
358     @mytime = 0                 # set in start method also
359     @sente = nil
360     @write_queue = Queue::new
361     @main_thread = Thread::current
362     @writer_thread = Thread::start do
363       Thread.pass
364       writer()
365     end
366   end
367
368   attr_accessor :socket, :status
369   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
370   attr_accessor :main_thread, :writer_thread, :write_queue
371   
372   def kill
373     log_message(sprintf("user %s killed", @name))
374     if (@game)
375       @game.kill(self)
376     end
377     finish
378     Thread::kill(@main_thread) if @main_thread
379   end
380
381   def finish
382     if (@status != "finished")
383       @status = "finished"
384       log_message(sprintf("user %s finish", @name))    
385       # TODO you should confirm that there is no message in the queue.
386       Thread::kill(@writer_thread) if @writer_thread
387       begin
388 #        @socket.close if (! @socket.closed?)
389       rescue
390         log_message(sprintf("user %s finish failed", @name))    
391       end
392     end
393   end
394
395   def write_safe(str)
396     @write_queue.push(str.gsub(/[\r\n]+/, @eol))
397   end
398
399   def writer
400     while (str = @write_queue.pop)
401       begin
402         @socket.write(str)
403       rescue Exception => ex
404         log_error("Failed to send a message to #{@name}.")
405         log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
406         return
407       end
408     end
409   end
410
411   def to_s
412     if ((status == "game_waiting") ||
413         (status == "start_waiting") ||
414         (status == "agree_waiting") ||
415         (status == "game"))
416       if (@sente)
417         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
418       elsif (@sente == false)
419         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
420       elsif (@sente == nil)
421         return sprintf("%s %s %s %s *", @name, @protocol, @status, @game_name)
422       end
423     else
424       return sprintf("%s %s %s", @name, @protocol, @status)
425     end
426   end
427
428   def run(csa_1st_str=nil)
429     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
430       begin
431         if (@writer_thread == nil || @writer_thread.status == false)
432           # The writer_thread has been killed because of socket errors.
433           return
434         end
435
436         $mutex.lock
437         if (csa_1st_str)
438           str = csa_1st_str
439           csa_1st_str = nil
440         end
441         if (@write_queue.size > Max_Write_Queue_Size)
442           log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
443                 return
444         end
445
446         if (@status == "finished")
447           return
448         end
449         str.chomp! if (str.class == String) # may be strip! ?
450         case str
451         when "" 
452           # Application-level protocol for Keep-Alive
453           # If the server gets LF, it sends back LF.
454           # 30 sec rule (client may not send LF again within 30 sec) is not implemented yet.
455           write_safe("\n")
456         when /^[\+\-][^%]/
457           if (@status == "game")
458             array_str = str.split(",")
459             move = array_str.shift
460             additional = array_str.shift
461             if /^'(.*)/ =~ additional
462               comment = array_str.unshift("'*#{$1}")
463             end
464             s = @game.handle_one_move(move, self)
465             @game.fh.print("#{comment}\n") if (comment && !s)
466             return if (s && @protocol == LoginCSA::PROTOCOL)
467           end
468         when /^%[^%]/, :timeout
469           if (@status == "game")
470             s = @game.handle_one_move(str, self)
471             return if (s && @protocol == LoginCSA::PROTOCOL)
472           # else
473           #   begin
474           #     @socket.write("##[KEEPALIVE] #{Time.now}\n")
475           #   rescue Exception => ex
476           #     log_error("Failed to send a keepalive to #{@name}.")
477           #     log_error("#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}")
478           #     return
479           #   end
480           end
481         when :exception
482           log_error("Failed to receive a message from #{@name}.")
483           return
484         when /^REJECT/
485           if (@status == "agree_waiting")
486             @game.reject(@name)
487             return if (@protocol == LoginCSA::PROTOCOL)
488           else
489             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
490           end
491         when /^AGREE/
492           if (@status == "agree_waiting")
493             @status = "start_waiting"
494             if ((@game.sente.status == "start_waiting") &&
495                 (@game.gote.status == "start_waiting"))
496               @game.start
497               @game.sente.status = "game"
498               @game.gote.status = "game"
499             end
500           else
501             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
502           end
503         when /^%%SHOW\s+(\S+)/
504           game_id = $1
505           if (LEAGUE.games[game_id])
506             write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
507           end
508           write_safe("##[SHOW] +OK\n")
509         when /^%%MONITORON\s+(\S+)/
510           game_id = $1
511           if (LEAGUE.games[game_id])
512             LEAGUE.games[game_id].monitoron(self)
513             write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
514             write_safe("##[MONITOR][#{game_id}] +OK\n")
515           end
516         when /^%%MONITOROFF\s+(\S+)/
517           game_id = $1
518           if (LEAGUE.games[game_id])
519             LEAGUE.games[game_id].monitoroff(self)
520           end
521         when /^%%HELP/
522           write_safe(
523             %!##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"\n!)
524         when /^%%RATING/
525           players = LEAGUE.rated_players
526           players.sort {|a,b| b.rate <=> a.rate}.each do |p|
527             write_safe("##[RATING] %s \t %4d @%s\n" % 
528                        [p.simple_id, p.rate, p.modified_at.strftime("%Y-%m-%d")])
529           end
530           write_safe("##[RATING] +OK\n")
531         when /^%%VERSION/
532           write_safe "##[VERSION] Shogi Server revision #{Revision}\n"
533           write_safe("##[VERSION] +OK\n")
534         when /^%%GAME\s*$/
535           if ((@status == "connected") || (@status == "game_waiting"))
536             @status = "connected"
537             @game_name = ""
538           else
539             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
540           end
541         when /^%%(GAME|CHALLENGE)\s+(\S+)\s+([\+\-\*])\s*$/
542           command_name = $1
543           game_name = $2
544           my_sente_str = $3
545           if (! Login::good_game_name?(game_name))
546             write_safe(sprintf("##[ERROR] bad game name\n"))
547             next
548           elsif ((@status == "connected") || (@status == "game_waiting"))
549             ## continue
550           else
551             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
552             next
553           end
554           if ((my_sente_str == "*") ||
555               (my_sente_str == "+") ||
556               (my_sente_str == "-"))
557             ## ok
558           else
559             write_safe(sprintf("##[ERROR] bad game option\n"))
560             next
561           end
562
563           if (my_sente_str == "*")
564             rival = LEAGUE.get_player("game_waiting", game_name, nil, self) # no preference
565           elsif (my_sente_str == "+")
566             rival = LEAGUE.get_player("game_waiting", game_name, false, self) # rival must be gote
567           elsif (my_sente_str == "-")
568             rival = LEAGUE.get_player("game_waiting", game_name, true, self) # rival must be sente
569           else
570             ## never reached
571           end
572           if (rival)
573             @game_name = game_name
574             if ((my_sente_str == "*") && (rival.sente == nil))
575               if (rand(2) == 0)
576                 @sente = true
577                 rival.sente = false
578               else
579                 @sente = false
580                 rival.sente = true
581               end
582             elsif (rival.sente == true) # rival has higher priority
583               @sente = false
584             elsif (rival.sente == false)
585               @sente = true
586             elsif (my_sente_str == "+")
587               @sente = true
588               rival.sente = false
589             elsif (my_sente_str == "-")
590               @sente = false
591               rival.sente = true
592             else
593               ## never reached
594             end
595             Game::new(@game_name, self, rival)
596             self.status = "agree_waiting"
597             rival.status = "agree_waiting"
598           else # rival not found
599             if (command_name == "GAME")
600               @status = "game_waiting"
601               @game_name = game_name
602               if (my_sente_str == "+")
603                 @sente = true
604               elsif (my_sente_str == "-")
605                 @sente = false
606               else
607                 @sente = nil
608               end
609             else                # challenge
610               write_safe(sprintf("##[ERROR] can't find rival for %s\n", game_name))
611               @status = "connected"
612               @game_name = ""
613               @sente = nil
614             end
615           end
616         when /^%%CHAT\s+(.+)/
617           message = $1
618           LEAGUE.players.each do |name, player|
619             if (player.protocol != LoginCSA::PROTOCOL)
620               player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
621             end
622           end
623         when /^%%LIST/
624           buf = Array::new
625           LEAGUE.games.each do |id, game|
626             buf.push(sprintf("##[LIST] %s\n", id))
627           end
628           buf.push("##[LIST] +OK\n")
629           write_safe(buf.join)
630         when /^%%WHO/
631           buf = Array::new
632           LEAGUE.players.each do |name, player|
633             buf.push(sprintf("##[WHO] %s\n", player.to_s))
634           end
635           buf.push("##[WHO] +OK\n")
636           write_safe(buf.join)
637         when /^LOGOUT/
638           @status = "connected"
639           write_safe("LOGOUT:completed\n")
640           return
641         when /^CHALLENGE/
642           # This command is only available for CSA's official testing server.
643           # So, this means nothing for this program.
644           write_safe("CHALLENGE ACCEPTED\n")
645         when /^\s*$/
646           ## ignore null string
647         else
648           msg = "##[ERROR] unknown command %s\n" % [str]
649           write_safe(msg)
650           log_error(msg)
651         end
652       ensure
653         $mutex.unlock
654       end
655     end                         # enf of while
656   end
657 end
658
659 class Piece
660   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
661   def initialize(board, x, y, sente, promoted=false)
662     @board = board
663     @x = x
664     @y = y
665     @sente = sente
666     @promoted = promoted
667
668     if ((x == 0) || (y == 0))
669       if (sente)
670         hands = board.sente_hands
671       else
672         hands = board.gote_hands
673       end
674       hands.push(self)
675       hands.sort! {|a, b|
676         a.name <=> b.name
677       }
678     else
679       @board.array[x][y] = self
680     end
681   end
682   attr_accessor :promoted, :sente, :x, :y, :board
683
684   def room_of_head?(x, y, name)
685     true
686   end
687
688   def movable_grids
689     return adjacent_movable_grids + far_movable_grids
690   end
691
692   def far_movable_grids
693     return []
694   end
695
696   def jump_to?(x, y)
697     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
698       if ((@board.array[x][y] == nil) || # dst is empty
699           (@board.array[x][y].sente != @sente)) # dst is enemy
700         return true
701       end
702     end
703     return false
704   end
705
706   def put_to?(x, y)
707     if ((1 <= x) && (x <= 9) && (1 <= y) && (y <= 9))
708       if (@board.array[x][y] == nil) # dst is empty?
709         return true
710       end
711     end
712     return false
713   end
714
715   def adjacent_movable_grids
716     grids = Array::new
717     if (@promoted)
718       moves = @promoted_moves
719     else
720       moves = @normal_moves
721     end
722     moves.each do |(dx, dy)|
723       if (@sente)
724         cand_y = @y - dy
725       else
726         cand_y = @y + dy
727       end
728       cand_x = @x + dx
729       if (jump_to?(cand_x, cand_y))
730         grids.push([cand_x, cand_y])
731       end
732     end
733     return grids
734   end
735
736   def move_to?(x, y, name)
737     return false if (! room_of_head?(x, y, name))
738     return false if ((name != @name) && (name != @promoted_name))
739     return false if (@promoted && (name != @promoted_name)) # can't un-promote
740
741     if (! @promoted)
742       return false if (((@x == 0) || (@y == 0)) && (name != @name)) # can't put promoted piece
743       if (@sente)
744         return false if ((4 <= @y) && (4 <= y) && (name != @name)) # can't promote
745       else
746         return false if ((6 >= @y) && (6 >= y) && (name != @name))
747       end
748     end
749
750     if ((@x == 0) || (@y == 0))
751       return jump_to?(x, y)
752     else
753       return movable_grids.include?([x, y])
754     end
755   end
756
757   def move_to(x, y)
758     if ((@x == 0) || (@y == 0))
759       if (@sente)
760         @board.sente_hands.delete(self)
761       else
762         @board.gote_hands.delete(self)
763       end
764       @board.array[x][y] = self
765     elsif ((x == 0) || (y == 0))
766       @promoted = false         # clear promoted flag before moving to hands
767       if (@sente)
768         @board.sente_hands.push(self)
769       else
770         @board.gote_hands.push(self)
771       end
772       @board.array[@x][@y] = nil
773     else
774       @board.array[@x][@y] = nil
775       @board.array[x][y] = self
776     end
777     @x = x
778     @y = y
779   end
780
781   def point
782     @point
783   end
784
785   def name
786     @name
787   end
788
789   def promoted_name
790     @promoted_name
791   end
792
793   def to_s
794     if (@sente)
795       sg = "+"
796     else
797       sg = "-"
798     end
799     if (@promoted)
800       n = @promoted_name
801     else
802       n = @name
803     end
804     return sg + n
805   end
806 end
807
808 class PieceFU < Piece
809   def initialize(*arg)
810     @point = 1
811     @normal_moves = [[0, +1]]
812     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
813     @name = "FU"
814     @promoted_name = "TO"
815     super
816   end
817   def room_of_head?(x, y, name)
818     if (name == "FU")
819       if (@sente)
820         return false if (y == 1)
821       else
822         return false if (y == 9)
823       end
824       ## 2fu check
825       c = 0
826       iy = 1
827       while (iy <= 9)
828         if ((iy  != @y) &&      # not source position
829             @board.array[x][iy] &&
830             (@board.array[x][iy].sente == @sente) && # mine
831             (@board.array[x][iy].name == "FU") &&
832             (@board.array[x][iy].promoted == false))
833           return false
834         end
835         iy = iy + 1
836       end
837     end
838     return true
839   end
840 end
841
842 class PieceKY  < Piece
843   def initialize(*arg)
844     @point = 1
845     @normal_moves = []
846     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
847     @name = "KY"
848     @promoted_name = "NY"
849     super
850   end
851   def room_of_head?(x, y, name)
852     if (name == "KY")
853       if (@sente)
854         return false if (y == 1)
855       else
856         return false if (y == 9)
857       end
858     end
859     return true
860   end
861   def far_movable_grids
862     grids = Array::new
863     if (@promoted)
864       return []
865     else
866       if (@sente)                 # up
867         cand_x = @x
868         cand_y = @y - 1
869         while (jump_to?(cand_x, cand_y))
870           grids.push([cand_x, cand_y])
871           break if (! put_to?(cand_x, cand_y))
872           cand_y = cand_y - 1
873         end
874       else                        # down
875         cand_x = @x
876         cand_y = @y + 1
877         while (jump_to?(cand_x, cand_y))
878           grids.push([cand_x, cand_y])
879           break if (! put_to?(cand_x, cand_y))
880           cand_y = cand_y + 1
881         end
882       end
883       return grids
884     end
885   end
886 end
887 class PieceKE  < Piece
888   def initialize(*arg)
889     @point = 1
890     @normal_moves = [[+1, +2], [-1, +2]]
891     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
892     @name = "KE"
893     @promoted_name = "NK"
894     super
895   end
896   def room_of_head?(x, y, name)
897     if (name == "KE")
898       if (@sente)
899         return false if ((y == 1) || (y == 2))
900       else
901         return false if ((y == 9) || (y == 8))
902       end
903     end
904     return true
905   end
906 end
907 class PieceGI  < Piece
908   def initialize(*arg)
909     @point = 1
910     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
911     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
912     @name = "GI"
913     @promoted_name = "NG"
914     super
915   end
916 end
917 class PieceKI  < Piece
918   def initialize(*arg)
919     @point = 1
920     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
921     @promoted_moves = []
922     @name = "KI"
923     @promoted_name = nil
924     super
925   end
926 end
927 class PieceKA  < Piece
928   def initialize(*arg)
929     @point = 5
930     @normal_moves = []
931     @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
932     @name = "KA"
933     @promoted_name = "UM"
934     super
935   end
936   def far_movable_grids
937     grids = Array::new
938     ## up right
939     cand_x = @x - 1
940     cand_y = @y - 1
941     while (jump_to?(cand_x, cand_y))
942       grids.push([cand_x, cand_y])
943       break if (! put_to?(cand_x, cand_y))
944       cand_x = cand_x - 1
945       cand_y = cand_y - 1
946     end
947     ## down right
948     cand_x = @x - 1
949     cand_y = @y + 1
950     while (jump_to?(cand_x, cand_y))
951       grids.push([cand_x, cand_y])
952       break if (! put_to?(cand_x, cand_y))
953       cand_x = cand_x - 1
954       cand_y = cand_y + 1
955     end
956     ## up left
957     cand_x = @x + 1
958     cand_y = @y - 1
959     while (jump_to?(cand_x, cand_y))
960       grids.push([cand_x, cand_y])
961       break if (! put_to?(cand_x, cand_y))
962       cand_x = cand_x + 1
963       cand_y = cand_y - 1
964     end
965     ## down left
966     cand_x = @x + 1
967     cand_y = @y + 1
968     while (jump_to?(cand_x, cand_y))
969       grids.push([cand_x, cand_y])
970       break if (! put_to?(cand_x, cand_y))
971       cand_x = cand_x + 1
972       cand_y = cand_y + 1
973     end
974     return grids
975   end
976 end
977 class PieceHI  < Piece
978   def initialize(*arg)
979     @point = 5
980     @normal_moves = []
981     @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
982     @name = "HI"
983     @promoted_name = "RY"
984     super
985   end
986   def far_movable_grids
987     grids = Array::new
988     ## up
989     cand_x = @x
990     cand_y = @y - 1
991     while (jump_to?(cand_x, cand_y))
992       grids.push([cand_x, cand_y])
993       break if (! put_to?(cand_x, cand_y))
994       cand_y = cand_y - 1
995     end
996     ## down
997     cand_x = @x
998     cand_y = @y + 1
999     while (jump_to?(cand_x, cand_y))
1000       grids.push([cand_x, cand_y])
1001       break if (! put_to?(cand_x, cand_y))
1002       cand_y = cand_y + 1
1003     end
1004     ## right
1005     cand_x = @x - 1
1006     cand_y = @y
1007     while (jump_to?(cand_x, cand_y))
1008       grids.push([cand_x, cand_y])
1009       break if (! put_to?(cand_x, cand_y))
1010       cand_x = cand_x - 1
1011     end
1012     ## down
1013     cand_x = @x + 1
1014     cand_y = @y
1015     while (jump_to?(cand_x, cand_y))
1016       grids.push([cand_x, cand_y])
1017       break if (! put_to?(cand_x, cand_y))
1018       cand_x = cand_x + 1
1019     end
1020     return grids
1021   end
1022 end
1023 class PieceOU < Piece
1024   def initialize(*arg)
1025     @point = 0
1026     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
1027     @promoted_moves = []
1028     @name = "OU"
1029     @promoted_name = nil
1030     super
1031   end
1032 end
1033
1034 class Board
1035   def initialize
1036     @sente_hands = Array::new
1037     @gote_hands = Array::new
1038     @history = Hash::new
1039     @sente_history = Hash::new
1040     @gote_history = Hash::new
1041     @array = [[], [], [], [], [], [], [], [], [], []]
1042     @move_count = 0
1043   end
1044   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
1045   attr_reader :move_count
1046
1047   def initial
1048     PieceKY::new(self, 1, 1, false)
1049     PieceKE::new(self, 2, 1, false)
1050     PieceGI::new(self, 3, 1, false)
1051     PieceKI::new(self, 4, 1, false)
1052     PieceOU::new(self, 5, 1, false)
1053     PieceKI::new(self, 6, 1, false)
1054     PieceGI::new(self, 7, 1, false)
1055     PieceKE::new(self, 8, 1, false)
1056     PieceKY::new(self, 9, 1, false)
1057     PieceKA::new(self, 2, 2, false)
1058     PieceHI::new(self, 8, 2, false)
1059     PieceFU::new(self, 1, 3, false)
1060     PieceFU::new(self, 2, 3, false)
1061     PieceFU::new(self, 3, 3, false)
1062     PieceFU::new(self, 4, 3, false)
1063     PieceFU::new(self, 5, 3, false)
1064     PieceFU::new(self, 6, 3, false)
1065     PieceFU::new(self, 7, 3, false)
1066     PieceFU::new(self, 8, 3, false)
1067     PieceFU::new(self, 9, 3, false)
1068
1069     PieceKY::new(self, 1, 9, true)
1070     PieceKE::new(self, 2, 9, true)
1071     PieceGI::new(self, 3, 9, true)
1072     PieceKI::new(self, 4, 9, true)
1073     PieceOU::new(self, 5, 9, true)
1074     PieceKI::new(self, 6, 9, true)
1075     PieceGI::new(self, 7, 9, true)
1076     PieceKE::new(self, 8, 9, true)
1077     PieceKY::new(self, 9, 9, true)
1078     PieceKA::new(self, 8, 8, true)
1079     PieceHI::new(self, 2, 8, true)
1080     PieceFU::new(self, 1, 7, true)
1081     PieceFU::new(self, 2, 7, true)
1082     PieceFU::new(self, 3, 7, true)
1083     PieceFU::new(self, 4, 7, true)
1084     PieceFU::new(self, 5, 7, true)
1085     PieceFU::new(self, 6, 7, true)
1086     PieceFU::new(self, 7, 7, true)
1087     PieceFU::new(self, 8, 7, true)
1088     PieceFU::new(self, 9, 7, true)
1089   end
1090
1091   def have_piece?(hands, name)
1092     piece = hands.find { |i|
1093       i.name == name
1094     }
1095     return piece
1096   end
1097
1098   def move_to(x0, y0, x1, y1, name, sente)
1099     if (sente)
1100       hands = @sente_hands
1101     else
1102       hands = @gote_hands
1103     end
1104
1105     if ((x0 == 0) || (y0 == 0))
1106       piece = have_piece?(hands, name)
1107       return :illegal if (! piece.move_to?(x1, y1, name))
1108       piece.move_to(x1, y1)
1109     else
1110       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
1111       if (@array[x0][y0].name != name) # promoted ?
1112         @array[x0][y0].promoted = true
1113       end
1114       if (@array[x1][y1])
1115         if (@array[x1][y1].name == "OU")
1116           return :outori        # return board update
1117         end
1118         @array[x1][y1].sente = @array[x0][y0].sente
1119         @array[x1][y1].move_to(0, 0)
1120         hands.sort! {|a, b|
1121           a.name <=> b.name
1122         }
1123       end
1124       @array[x0][y0].move_to(x1, y1)
1125     end
1126     @move_count += 1
1127     return true
1128   end
1129
1130   def look_for_ou(sente)
1131     x = 1
1132     while (x <= 9)
1133       y = 1
1134       while (y <= 9)
1135         if (@array[x][y] &&
1136             (@array[x][y].name == "OU") &&
1137             (@array[x][y].sente == sente))
1138           return @array[x][y]
1139         end
1140         y = y + 1
1141       end
1142       x = x + 1
1143     end
1144     raise "can't find ou"
1145   end
1146
1147   def checkmated?(sente)        # sente is loosing
1148     ou = look_for_ou(sente)
1149     x = 1
1150     while (x <= 9)
1151       y = 1
1152       while (y <= 9)
1153         if (@array[x][y] &&
1154             (@array[x][y].sente != sente))
1155           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
1156             return true
1157           end
1158         end
1159         y = y + 1
1160       end
1161       x = x + 1
1162     end
1163     return false
1164   end
1165
1166   def uchifuzume?(sente)
1167     rival_ou = look_for_ou(! sente)   # rival's ou
1168     if (sente)                  # rival is gote
1169       if ((rival_ou.y != 9) &&
1170           (@array[rival_ou.x][rival_ou.y + 1]) &&
1171           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
1172           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
1173         fu_x = rival_ou.x
1174         fu_y = rival_ou.y + 1
1175       else
1176         return false
1177       end
1178     else                        # gote
1179       if ((rival_ou.y != 0) &&
1180           (@array[rival_ou.x][rival_ou.y - 1]) &&
1181           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
1182           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
1183         fu_x = rival_ou.x
1184         fu_y = rival_ou.y - 1
1185       else
1186         return false
1187       end
1188     end
1189     
1190     ## case: rival_ou is moving
1191     escaped = false
1192     rival_ou.movable_grids.each do |(cand_x, cand_y)|
1193       tmp_board = Marshal.load(Marshal.dump(self))
1194       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
1195       raise "internal error" if (s != true)
1196       if (! tmp_board.checkmated?(! sente)) # good move
1197         return false
1198       end
1199     end
1200
1201     ## case: rival is capturing fu
1202     x = 1
1203     while (x <= 9)
1204       y = 1
1205       while (y <= 9)
1206         if (@array[x][y] &&
1207             (@array[x][y].sente != sente) &&
1208             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
1209           if (@array[x][y].promoted)
1210             name = @array[x][y].promoted_name
1211           else
1212             name = @array[x][y].name
1213           end
1214           tmp_board = Marshal.load(Marshal.dump(self))
1215           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
1216           raise "internal error" if (s != true)
1217           if (! tmp_board.checkmated?(! sente)) # good move
1218             return false
1219           end
1220         end
1221         y = y + 1
1222       end
1223       x = x + 1
1224     end
1225     return true
1226   end
1227
1228   def oute_sennichite?(sente)
1229     if (checkmated?(! sente))
1230       str = to_s
1231       if (sente)
1232         if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
1233           return true
1234         end
1235       else
1236         if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
1237           return true
1238         end
1239       end
1240     end
1241     return false
1242   end
1243
1244   def sennichite?(sente)
1245     str = to_s
1246     if (@history[str] && (@history[str] >= 3)) # already 3 times
1247       return true
1248     end
1249     return false
1250   end
1251
1252   def good_kachi?(sente)
1253     if (checkmated?(sente))
1254       puts "'NG: Checkmating." if $DEBUG
1255       return false 
1256     end
1257     
1258     ou = look_for_ou(sente)
1259     if (sente && (ou.y >= 4))
1260       puts "'NG: Black's OU does not enter yet." if $DEBUG
1261       return false     
1262     end  
1263     if (! sente && (ou.y <= 6))
1264       puts "'NG: White's OU does not enter yet." if $DEBUG
1265       return false 
1266     end
1267       
1268     number = 0
1269     point = 0
1270
1271     if (sente)
1272       hands = @sente_hands
1273       r = [1, 2, 3]
1274     else
1275       hands = @gote_hands
1276       r = [7, 8, 9]
1277     end
1278     r.each do |y|
1279       x = 1
1280       while (x <= 9)
1281         if (@array[x][y] &&
1282             (@array[x][y].sente == sente) &&
1283             (@array[x][y].point > 0))
1284           point = point + @array[x][y].point
1285           number = number + 1
1286         end
1287         x = x + 1
1288       end
1289     end
1290     hands.each do |piece|
1291       point = point + piece.point
1292     end
1293
1294     if (number < 10)
1295       puts "'NG: Piece#[%d] is too small." % [number] if $DEBUG
1296       return false     
1297     end  
1298     if (sente)
1299       if (point < 28)
1300         puts "'NG: Black's point#[%d] is too small." % [point] if $DEBUG
1301         return false 
1302       end  
1303     else
1304       if (point < 27)
1305         puts "'NG: White's point#[%d] is too small." % [point] if $DEBUG
1306         return false 
1307       end
1308     end
1309
1310     puts "'Good: Piece#[%d], Point[%d]." % [number, point] if $DEBUG
1311     return true
1312   end
1313
1314   # sente is nil only if tests in test_board run
1315   def handle_one_move(str, sente=nil)
1316     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1317       sg = $1
1318       x0 = $2.to_i
1319       y0 = $3.to_i
1320       x1 = $4.to_i
1321       y1 = $5.to_i
1322       name = $6
1323     elsif (str =~ /^%KACHI/)
1324       raise ArgumentError, "sente is null", caller if sente == nil
1325       if (good_kachi?(sente))
1326         return :kachi_win
1327       else
1328         return :kachi_lose
1329       end
1330     elsif (str =~ /^%TORYO/)
1331       return :toryo
1332     else
1333       return :illegal
1334     end
1335     
1336     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1337         ((x0 != 0) || (y0 != 0)))
1338       return :illegal
1339     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1340       return :illegal
1341     end
1342     
1343
1344     if (sg == "+")
1345       sente = true if sente == nil           # deprecated
1346       return :illegal unless sente == true   # black player's move must be black
1347       hands = @sente_hands
1348     else
1349       sente = false if sente == nil          # deprecated
1350       return :illegal unless sente == false  # white player's move must be white
1351       hands = @gote_hands
1352     end
1353     
1354     ## source check
1355     if ((x0 == 0) && (y0 == 0))
1356       return :illegal if (! have_piece?(hands, name))
1357     elsif (! @array[x0][y0])
1358       return :illegal           # no piece
1359     elsif (@array[x0][y0].sente != sente)
1360       return :illegal           # this is not mine
1361     elsif (@array[x0][y0].name != name)
1362       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1363     end
1364
1365     ## destination check
1366     if (@array[x1][y1] &&
1367         (@array[x1][y1].sente == sente)) # can't capture mine
1368       return :illegal
1369     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1370       return :illegal           # can't put on existing piece
1371     end
1372
1373     tmp_board = Marshal.load(Marshal.dump(self))
1374     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1375     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1376     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1377     return :sennichite if tmp_board.sennichite?(sente)
1378
1379     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1380       return :uchifuzume
1381     end
1382
1383     move_to(x0, y0, x1, y1, name, sente)
1384     str = to_s
1385
1386     if (checkmated?(! sente))
1387       if (sente)
1388         @sente_history[str] = (@sente_history[str] || 0) + 1
1389       else
1390         @gote_history[str] = (@gote_history[str] || 0) + 1
1391       end
1392     else
1393       if (sente)
1394         @sente_history.clear
1395       else
1396         @gote_history.clear
1397       end
1398     end
1399     @history[str] = (@history[str] || 0) + 1
1400     return :normal
1401   end
1402
1403   def to_s
1404     a = Array::new
1405     y = 1
1406     while (y <= 9)
1407       a.push(sprintf("P%d", y))
1408       x = 9
1409       while (x >= 1)
1410         piece = @array[x][y]
1411         if (piece)
1412           s = piece.to_s
1413         else
1414           s = " * "
1415         end
1416         a.push(s)
1417         x = x - 1
1418       end
1419       a.push(sprintf("\n"))
1420       y = y + 1
1421     end
1422     if (! sente_hands.empty?)
1423       a.push("P+")
1424       sente_hands.each do |p|
1425         a.push("00" + p.name)
1426       end
1427       a.push("\n")
1428     end
1429     if (! gote_hands.empty?)
1430       a.push("P-")
1431       gote_hands.each do |p|
1432         a.push("00" + p.name)
1433       end
1434       a.push("\n")
1435     end
1436     a.push("+\n")
1437     return a.join
1438   end
1439 end
1440
1441 class GameResult
1442   attr_reader :players, :black, :white
1443
1444   def initialize(p1, p2)
1445     @players = []
1446     @players << p1
1447     @players << p2
1448     if p1.sente && !p2.sente
1449       @black, @white = p1, p2
1450     elsif !p1.sente && p2.sente
1451       @black, @white = p2, p1
1452     else
1453       raise "Never reached!"
1454     end
1455   end
1456 end
1457
1458 class GameResultWin < GameResult
1459   attr_reader :winner, :loser
1460
1461   def initialize(winner, loser)
1462     super
1463     @winner, @loser = winner, loser
1464   end
1465
1466   def to_s
1467     black_name = @black.id || @black.name
1468     white_name = @white.id || @white.name
1469     "%s:%s" % [black_name, white_name]
1470   end
1471 end
1472
1473 class GameResultDraw < GameResult
1474
1475 end
1476
1477 class Game
1478   @@mutex = Mutex.new
1479   @@time  = 0
1480
1481   def initialize(game_name, player0, player1)
1482     @monitors = Array::new
1483     @game_name = game_name
1484     if (@game_name =~ /-(\d+)-(\d+)$/)
1485       @total_time = $1.to_i
1486       @byoyomi = $2.to_i
1487     end
1488
1489     if (player0.sente)
1490       @sente = player0
1491       @gote = player1
1492     else
1493       @sente = player1
1494       @gote = player0
1495     end
1496     @current_player = @sente
1497     @next_player = @gote
1498
1499     @sente.game = self
1500     @gote.game = self
1501
1502     @last_move = ""
1503     @current_turn = 0
1504
1505     @sente.status = "agree_waiting"
1506     @gote.status = "agree_waiting"
1507     
1508     @id = sprintf("%s+%s+%s+%s+%s", 
1509                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1510     @logfile = File.join(LEAGUE.dir, @id + ".csa")
1511
1512     LEAGUE.games[@id] = self
1513
1514     log_message(sprintf("game created %s", @id))
1515
1516     @board = Board::new
1517     @board.initial
1518     @start_time = nil
1519     @fh = nil
1520     @result = nil
1521
1522     propose
1523   end
1524   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1525   attr_accessor :last_move, :current_turn
1526   attr_reader   :result
1527
1528   def rated?
1529     @sente.rated? && @gote.rated?
1530   end
1531
1532   def monitoron(monitor)
1533     @monitors.delete(monitor)
1534     @monitors.push(monitor)
1535   end
1536
1537   def monitoroff(monitor)
1538     @monitors.delete(monitor)
1539   end
1540
1541   def reject(rejector)
1542     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1543     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1544     finish
1545   end
1546
1547   def kill(killer)
1548     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1549       reject(killer.name)
1550     elsif (@current_player == killer)
1551       abnormal_lose()
1552       finish
1553     end
1554   end
1555
1556   def finish
1557     log_message(sprintf("game finished %s", @id))
1558     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1559     @fh.close
1560
1561     @sente.game = nil
1562     @gote.game = nil
1563     @sente.status = "connected"
1564     @gote.status = "connected"
1565
1566     if (@current_player.protocol == LoginCSA::PROTOCOL)
1567       @current_player.finish
1568     end
1569     if (@next_player.protocol == LoginCSA::PROTOCOL)
1570       @next_player.finish
1571     end
1572     @monitors = Array::new
1573     @sente = nil
1574     @gote = nil
1575     @current_player = nil
1576     @next_player = nil
1577     LEAGUE.games.delete(@id)
1578   end
1579
1580   def handle_one_move(str, player)
1581     finish_flag = true
1582     if (@current_player == player)
1583       @end_time = Time::new
1584       t = (@end_time - @start_time).floor
1585       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1586       
1587       move_status = nil
1588       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1589         status = :timeout
1590       elsif (str == :timeout)
1591         return false            # time isn't expired. players aren't swapped. continue game
1592       else
1593         @current_player.mytime = @current_player.mytime - t
1594         if (@current_player.mytime < 0)
1595           @current_player.mytime = 0
1596         end
1597
1598 #        begin
1599           move_status = @board.handle_one_move(str, @sente == @current_player)
1600 #        rescue
1601 #          log_error("handle_one_move raise exception for #{str}")
1602 #          move_status = :illegal
1603 #        end
1604
1605         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1606           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1607         else
1608           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1609             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1610             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1611             @fh.printf("%s\nT%d\n", str, t)
1612             @last_move = sprintf("%s,T%d", str, t)
1613             @current_turn = @current_turn + 1
1614           end
1615
1616           @monitors.each do |monitor|
1617             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1618             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1619           end
1620         end
1621       end
1622
1623       if (@next_player.status != "game") # rival is logout or disconnected
1624         abnormal_win()
1625       elsif (status == :timeout)
1626         timeout_lose()
1627       elsif (move_status == :illegal)
1628         illegal_lose()
1629       elsif (move_status == :kachi_win)
1630         kachi_win()
1631       elsif (move_status == :kachi_lose)
1632         kachi_lose()
1633       elsif (move_status == :toryo)
1634         toryo_lose()
1635       elsif (move_status == :outori)
1636         outori_win()
1637       elsif (move_status == :sennichite)
1638         sennichite_draw()
1639       elsif (move_status == :oute_sennichite)
1640         oute_sennichite_lose()
1641       elsif (move_status == :uchifuzume)
1642         uchifuzume_lose()
1643       elsif (move_status == :oute_kaihimore)
1644         oute_kaihimore_lose()
1645       else
1646         finish_flag = false
1647       end
1648       finish() if finish_flag
1649       (@current_player, @next_player) = [@next_player, @current_player]
1650       @start_time = Time::new
1651       return finish_flag
1652     end
1653   end
1654
1655   def abnormal_win
1656     @current_player.status = "connected"
1657     @next_player.status = "connected"
1658     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1659     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1660     @fh.printf("%%TORYO\n")
1661     @fh.print(@board.to_s.gsub(/^/, "\'"))
1662     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1663     @result = GameResultWin.new(@current_player, @next_player)
1664     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1665     @monitors.each do |monitor|
1666       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1667     end
1668   end
1669
1670   def abnormal_lose
1671     @current_player.status = "connected"
1672     @next_player.status = "connected"
1673     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1674     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1675     @fh.printf("%%TORYO\n")
1676     @fh.print(@board.to_s.gsub(/^/, "\'"))
1677     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1678     @result = GameResultWin.new(@next_player, @current_player)
1679     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1680     @monitors.each do |monitor|
1681       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1682     end
1683   end
1684
1685   def sennichite_draw
1686     @current_player.status = "connected"
1687     @next_player.status = "connected"
1688     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1689     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1690     @fh.print(@board.to_s.gsub(/^/, "\'"))
1691     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1692     @result = GameResultDraw.new(@current_player, @next_player)
1693     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1694     @monitors.each do |monitor|
1695       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1696     end
1697   end
1698
1699   def oute_sennichite_lose
1700     @current_player.status = "connected"
1701     @next_player.status = "connected"
1702     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1703     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1704     @fh.print(@board.to_s.gsub(/^/, "\'"))
1705     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1706     @result = GameResultWin.new(@next_player, @current_player)
1707     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1708     @monitors.each do |monitor|
1709       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1710     end
1711   end
1712
1713   def illegal_lose
1714     @current_player.status = "connected"
1715     @next_player.status = "connected"
1716     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1717     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1718     @fh.print(@board.to_s.gsub(/^/, "\'"))
1719     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1720     @result = GameResultWin.new(@next_player, @current_player)
1721     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1722     @monitors.each do |monitor|
1723       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1724     end
1725   end
1726
1727   def uchifuzume_lose
1728     @current_player.status = "connected"
1729     @next_player.status = "connected"
1730     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1731     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1732     @fh.print(@board.to_s.gsub(/^/, "\'"))
1733     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1734     @result = GameResultWin.new(@next_player, @current_player)
1735     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1736     @monitors.each do |monitor|
1737       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1738     end
1739   end
1740
1741   def oute_kaihimore_lose
1742     @current_player.status = "connected"
1743     @next_player.status = "connected"
1744     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1745     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1746     @fh.print(@board.to_s.gsub(/^/, "\'"))
1747     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1748     @result = GameResultWin.new(@next_player, @current_player)
1749     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1750     @monitors.each do |monitor|
1751       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1752     end
1753   end
1754
1755   def timeout_lose
1756     @current_player.status = "connected"
1757     @next_player.status = "connected"
1758     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1759     @next_player.write_safe("#TIME_UP\n#WIN\n")
1760     @fh.print(@board.to_s.gsub(/^/, "\'"))
1761     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1762     @result = GameResultWin.new(@next_player, @current_player)
1763     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1764     @monitors.each do |monitor|
1765       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1766     end
1767   end
1768
1769   def kachi_win
1770     @current_player.status = "connected"
1771     @next_player.status = "connected"
1772     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1773     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1774     @fh.printf("%%KACHI\n")
1775     @fh.print(@board.to_s.gsub(/^/, "\'"))
1776     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1777     @result = GameResultWin.new(@current_player, @next_player)
1778     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1779     @monitors.each do |monitor|
1780       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1781     end
1782   end
1783
1784   def kachi_lose
1785     @current_player.status = "connected"
1786     @next_player.status = "connected"
1787     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1788     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1789     @fh.printf("%%KACHI\n")
1790     @fh.print(@board.to_s.gsub(/^/, "\'"))
1791     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1792     @result = GameResultWin.new(@next_player, @current_player)
1793     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1794     @monitors.each do |monitor|
1795       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1796     end
1797   end
1798
1799   def toryo_lose
1800     @current_player.status = "connected"
1801     @next_player.status = "connected"
1802     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1803     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1804     @fh.printf("%%TORYO\n")
1805     @fh.print(@board.to_s.gsub(/^/, "\'"))
1806     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1807     @result = GameResultWin.new(@next_player, @current_player)
1808     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1809     @monitors.each do |monitor|
1810       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1811     end
1812   end
1813
1814   def outori_win
1815     @current_player.status = "connected"
1816     @next_player.status = "connected"
1817     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1818     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1819     @fh.print(@board.to_s.gsub(/^/, "\'"))
1820     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1821     @result = GameResultWin.new(@current_player, @next_player)
1822     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1823     @monitors.each do |monitor|
1824       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1825     end
1826   end
1827
1828   def start
1829     log_message(sprintf("game started %s", @id))
1830     @sente.write_safe(sprintf("START:%s\n", @id))
1831     @gote.write_safe(sprintf("START:%s\n", @id))
1832     @sente.mytime = @total_time
1833     @gote.mytime = @total_time
1834     @start_time = Time::new
1835   end
1836
1837   def propose
1838     begin
1839       @fh = open(@logfile, "w")
1840       @fh.sync = true
1841
1842       @fh.printf("V2\n")
1843       @fh.printf("N+%s\n", @sente.name)
1844       @fh.printf("N-%s\n", @gote.name)
1845       @fh.printf("$EVENT:%s\n", @id)
1846
1847       @sente.write_safe(propose_message("+"))
1848       @gote.write_safe(propose_message("-"))
1849
1850       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1851       @fh.print <<EOM
1852 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1853 P2 * -HI *  *  *  *  * -KA * 
1854 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1855 P4 *  *  *  *  *  *  *  *  * 
1856 P5 *  *  *  *  *  *  *  *  * 
1857 P6 *  *  *  *  *  *  *  *  * 
1858 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1859 P8 * +KA *  *  *  *  * +HI * 
1860 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1861 +
1862 EOM
1863     end
1864   end
1865
1866   def show()
1867     str0 = <<EOM
1868 BEGIN Game_Summary
1869 Protocol_Version:1.1
1870 Protocol_Mode:Server
1871 Format:Shogi 1.0
1872 Declaration:Jishogi 1.1
1873 Game_ID:#{@id}
1874 Name+:#{@sente.name}
1875 Name-:#{@gote.name}
1876 Rematch_On_Draw:NO
1877 To_Move:+
1878 BEGIN Time
1879 Time_Unit:1sec
1880 Total_Time:#{@total_time}
1881 Byoyomi:#{@byoyomi}
1882 Least_Time_Per_Move:#{Least_Time_Per_Move}
1883 Remaining_Time+:#{@sente.mytime}
1884 Remaining_Time-:#{@gote.mytime}
1885 Last_Move:#{@last_move}
1886 Current_Turn:#{@current_turn}
1887 END Time
1888 BEGIN Position
1889 EOM
1890
1891     str1 = <<EOM
1892 END Position
1893 END Game_Summary
1894 EOM
1895
1896     return str0 + @board.to_s + str1
1897   end
1898
1899   def propose_message(sg_flag)
1900     str = <<EOM
1901 BEGIN Game_Summary
1902 Protocol_Version:1.1
1903 Protocol_Mode:Server
1904 Format:Shogi 1.0
1905 Declaration:Jishogi 1.1
1906 Game_ID:#{@id}
1907 Name+:#{@sente.name}
1908 Name-:#{@gote.name}
1909 Your_Turn:#{sg_flag}
1910 Rematch_On_Draw:NO
1911 To_Move:+
1912 BEGIN Time
1913 Time_Unit:1sec
1914 Total_Time:#{@total_time}
1915 Byoyomi:#{@byoyomi}
1916 Least_Time_Per_Move:#{Least_Time_Per_Move}
1917 END Time
1918 BEGIN Position
1919 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1920 P2 * -HI *  *  *  *  * -KA * 
1921 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1922 P4 *  *  *  *  *  *  *  *  * 
1923 P5 *  *  *  *  *  *  *  *  * 
1924 P6 *  *  *  *  *  *  *  *  * 
1925 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1926 P8 * +KA *  *  *  *  * +HI * 
1927 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1928 P+
1929 P-
1930 +
1931 END Position
1932 END Game_Summary
1933 EOM
1934     return str
1935   end
1936   
1937   private
1938   
1939   def issue_current_time
1940     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1941     @@mutex.synchronize do
1942       while time <= @@time do
1943         time += 1
1944       end
1945       @@time = time
1946     end
1947   end
1948 end
1949 end # module ShogiServer
1950
1951 #################################################
1952 # MAIN
1953 #
1954
1955 def usage
1956     print <<EOM
1957 NAME
1958         shogi-server - server for CSA server protocol
1959
1960 SYNOPSIS
1961         shogi-server [OPTIONS] event_name port_number
1962
1963 DESCRIPTION
1964         server for CSA server protocol
1965
1966 OPTIONS
1967         --pid-file file
1968                 specify filename for logging process ID
1969     --daemon dir
1970         run as a daemon. Log files will be put in dir.
1971
1972 LICENSE
1973         this file is distributed under GPL version2 and might be compiled by Exerb
1974
1975 SEE ALSO
1976
1977 RELEASE
1978         #{ShogiServer::Release}
1979
1980 REVISION
1981         #{ShogiServer::Revision}
1982 EOM
1983 end
1984
1985 def log_message(str)
1986   $logger.info(str)
1987 end
1988
1989 def log_warning(str)
1990   $logger.warn(str)
1991 end
1992
1993 def log_error(str)
1994   $logger.error(str)
1995 end
1996
1997
1998 def parse_command_line
1999   options = Hash::new
2000   parser = GetoptLong.new( ["--daemon",         GetoptLong::REQUIRED_ARGUMENT],
2001                            ["--pid-file",       GetoptLong::REQUIRED_ARGUMENT]
2002                          )
2003   parser.quiet = true
2004   begin
2005     parser.each_option do |name, arg|
2006       name.sub!(/^--/, '')
2007       options[name] = arg.dup
2008     end
2009   rescue
2010     usage
2011     raise parser.error_message
2012   end
2013   return options
2014 end
2015
2016 def write_pid_file(file)
2017   open(file, "w") do |fh|
2018     fh.print Process::pid, "\n"
2019   end
2020 end
2021
2022 def mutex_watchdog(mutex, sec)
2023   while true
2024     begin
2025       timeout(sec) do
2026         begin
2027           mutex.lock
2028         ensure
2029           mutex.unlock
2030         end
2031       end
2032       sleep(sec)
2033     rescue TimeoutError
2034       log_error("mutex watchdog timeout")
2035       exit(1)
2036     end
2037   end
2038 end
2039
2040 def main
2041
2042   $mutex = Mutex::new
2043   Thread::start do
2044     Thread.pass
2045     mutex_watchdog($mutex, 10)
2046   end
2047
2048   $options = parse_command_line
2049   if (ARGV.length != 2)
2050     usage
2051     exit 2
2052   end
2053
2054   LEAGUE.event = ARGV.shift
2055   port = ARGV.shift
2056
2057   write_pid_file($options["pid-file"]) if ($options["pid-file"])
2058
2059   dir = $options["daemon"] || nil
2060   if dir && ! File.exist?(dir)
2061     FileUtils.mkdir(dir)
2062   end
2063   log_file = dir ? File.join(dir, "shogi-server.log") : STDOUT
2064   $logger = WEBrick::Log.new(log_file)
2065
2066   LEAGUE.dir = dir || File.dirname(__FILE__)
2067   LEAGUE.setup_players_database
2068
2069   config = {}
2070   config[:Port]       = port
2071   config[:ServerType] = WEBrick::Daemon if $options["daemon"]
2072   config[:Logger]     = $logger
2073
2074   server = WEBrick::GenericServer.new(config)
2075   ["INT", "TERM"].each {|signal| trap(signal){ server.shutdown } }
2076   $stderr.puts("server started as a deamon") if $options["daemon"] 
2077   log_message("server started")
2078
2079   server.start do |client|
2080       # client.sync = true # this is already set in WEBrick 
2081       client.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
2082         # Keepalive time can be set by /proc/sys/net/ipv4/tcp_keepalive_time
2083       player = nil
2084       login  = nil
2085       while (str = client.gets_timeout(ShogiServer::Login_Time))
2086         begin
2087           $mutex.lock
2088           str =~ /([\r\n]*)$/
2089           eol = $1
2090           if (ShogiServer::Login::good_login?(str))
2091             player = ShogiServer::Player::new(str, client)
2092             player.eol = eol
2093             login  = ShogiServer::Login::factory(str, player)
2094             if (LEAGUE.players[player.name])
2095               if ((LEAGUE.players[player.name].password == player.password) &&
2096                   (LEAGUE.players[player.name].status != "game"))
2097                 log_message(sprintf("user %s login forcely", player.name))
2098                 LEAGUE.players[player.name].kill
2099               else
2100                 login.incorrect_duplicated_player(str)
2101                 #Thread::exit
2102                 #return
2103                 # TODO
2104                 player = nil
2105                 break
2106               end
2107             end
2108             LEAGUE.add(player)
2109             break
2110           else
2111             client.write_safe("LOGIN:incorrect" + eol)
2112             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2113           end
2114         ensure
2115           $mutex.unlock
2116         end
2117       end                       # login loop
2118       if (! player)
2119         #client.close
2120         #Thread::exit
2121         #return
2122         next
2123       end
2124       log_message(sprintf("user %s login", player.name))
2125       login.process
2126       player.run(login.csa_1st_str)
2127       begin
2128         $mutex.lock
2129         if (player.game)
2130           player.game.kill(player)
2131         end
2132         player.finish # socket has been closed
2133         LEAGUE.delete(player)
2134         log_message(sprintf("user %s logout", player.name))
2135       ensure
2136         $mutex.unlock
2137       end
2138   end
2139 end
2140
2141
2142 if ($0 == __FILE__)
2143   STDOUT.sync = true
2144   STDERR.sync = true
2145   TCPSocket.do_not_reverse_lookup = true
2146   Thread.abort_on_exception = true
2147
2148   LEAGUE = ShogiServer::League::new
2149   main
2150 end