OSDN Git Service

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