OSDN Git Service

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