OSDN Git Service

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