OSDN Git Service

* Change the player id, which is now <name>+<hash_of_the_trip>.
[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)
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       if (good_kachi?(sente))
1152         return :kachi_win
1153       else
1154         return :kachi_lose
1155       end
1156     elsif (str =~ /^%TORYO/)
1157       return :toryo
1158     else
1159       return :illegal
1160     end
1161     
1162     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1163         ((x0 != 0) || (y0 != 0)))
1164       return :illegal
1165     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1166       return :illegal
1167     end
1168     
1169     if (sg == "+")
1170       sente = true
1171       hands = @sente_hands
1172     else
1173       sente = false
1174       hands = @gote_hands
1175     end
1176     
1177     ## source check
1178     if ((x0 == 0) && (y0 == 0))
1179       return :illegal if (! have_piece?(hands, name))
1180     elsif (! @array[x0][y0])
1181       return :illegal           # no piece
1182     elsif (@array[x0][y0].sente != sente)
1183       return :illegal           # this is not mine
1184     elsif (@array[x0][y0].name != name)
1185       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1186     end
1187
1188     ## destination check
1189     if (@array[x1][y1] &&
1190         (@array[x1][y1].sente == sente)) # can't capture mine
1191       return :illegal
1192     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1193       return :illegal           # can't put on existing piece
1194     end
1195
1196     tmp_board = Marshal.load(Marshal.dump(self))
1197     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1198     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1199     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1200     return :sennichite if tmp_board.sennichite?(sente)
1201
1202     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1203       return :uchifuzume
1204     end
1205
1206     move_to(x0, y0, x1, y1, name, sente)
1207     str = to_s
1208
1209     if (checkmated?(! sente))
1210       if (sente)
1211         @sente_history[str] = (@sente_history[str] || 0) + 1
1212       else
1213         @gote_history[str] = (@gote_history[str] || 0) + 1
1214       end
1215     else
1216       if (sente)
1217         @sente_history.clear
1218       else
1219         @gote_history.clear
1220       end
1221     end
1222     @history[str] = (@history[str] || 0) + 1
1223     return :normal
1224   end
1225
1226   def to_s
1227     a = Array::new
1228     y = 1
1229     while (y <= 9)
1230       a.push(sprintf("P%d", y))
1231       x = 9
1232       while (x >= 1)
1233         piece = @array[x][y]
1234         if (piece)
1235           s = piece.to_s
1236         else
1237           s = " * "
1238         end
1239         a.push(s)
1240         x = x - 1
1241       end
1242       a.push(sprintf("\n"))
1243       y = y + 1
1244     end
1245     if (! sente_hands.empty?)
1246       a.push("P+")
1247       sente_hands.each do |p|
1248         a.push("00" + p.name)
1249       end
1250       a.push("\n")
1251     end
1252     if (! gote_hands.empty?)
1253       a.push("P-")
1254       gote_hands.each do |p|
1255         a.push("00" + p.name)
1256       end
1257       a.push("\n")
1258     end
1259     a.push("+\n")
1260     return a.join
1261   end
1262 end
1263
1264 class GameResult
1265   attr_reader :players, :black, :white
1266
1267   def initialize(p1, p2)
1268     @players = []
1269     @players << p1
1270     @players << p2
1271     if p1.sente && !p2.sente
1272       @black, @white = p1, p2
1273     elsif !p1.sente && p2.sente
1274       @black, @white = p2, p1
1275     else
1276       raise "Never reached!"
1277     end
1278   end
1279 end
1280
1281 class GameResultWin < GameResult
1282   WIN_MARK  = "o"
1283   LOSS_MARK = "x"
1284   attr_reader :winner, :loser
1285     
1286   def initialize(winner, loser)
1287     super
1288     @winner, @loser = winner, loser
1289   end
1290
1291   def to_s
1292     if @black == @winner
1293       black_mark, white_mark = WIN_MARK, LOSS_MARK
1294     elsif @white == @winner
1295       black_mark, white_mark = LOSS_MARK, WIN_MARK
1296     else
1297       raise "Never reached! Nobody is the winner."
1298     end
1299     black_name = @black.id || @black.name
1300     white_name = @white.id || @white.name
1301     "%s : %s %d : %s %d : %s" % 
1302       [black_mark, black_name, @black.rate, 
1303        white_name, @white.rate, white_mark]
1304   end
1305 end
1306
1307 class GameResultDraw < GameResult
1308
1309 end
1310
1311 class Game
1312   @@mutex = Mutex.new
1313   @@time  = 0
1314
1315   def initialize(game_name, player0, player1)
1316     @monitors = Array::new
1317     @game_name = game_name
1318     if (@game_name =~ /-(\d+)-(\d+)$/)
1319       @total_time = $1.to_i
1320       @byoyomi = $2.to_i
1321     end
1322
1323     if (player0.sente)
1324       @sente = player0
1325       @gote = player1
1326     else
1327       @sente = player1
1328       @gote = player0
1329     end
1330     @current_player = @sente
1331     @next_player = @gote
1332
1333     @sente.game = self
1334     @gote.game = self
1335
1336     @last_move = ""
1337     @current_turn = 0
1338
1339     @sente.status = "agree_waiting"
1340     @gote.status = "agree_waiting"
1341     
1342     @id = sprintf("%s+%s+%s+%s+%s", 
1343                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1344     @logfile = @id + ".csa"
1345
1346     LEAGUE.games[@id] = self
1347
1348     log_message(sprintf("game created %s", @id))
1349
1350     @board = Board::new
1351     @board.initial
1352     @start_time = nil
1353     @fh = nil
1354     @result = nil
1355
1356     propose
1357   end
1358   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1359   attr_accessor :last_move, :current_turn
1360   attr_reader   :result
1361
1362   def rated?
1363     @sente.rated? && @gote.rated?
1364   end
1365
1366   def monitoron(monitor)
1367     @monitors.delete(monitor)
1368     @monitors.push(monitor)
1369   end
1370
1371   def monitoroff(monitor)
1372     @monitors.delete(monitor)
1373   end
1374
1375   def reject(rejector)
1376     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1377     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1378     finish
1379   end
1380
1381   def kill(killer)
1382     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1383       reject(killer.name)
1384     elsif (@current_player == killer)
1385       abnormal_lose()
1386       finish
1387     end
1388   end
1389
1390   def finish
1391     log_message(sprintf("game finished %s", @id))
1392     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1393     @fh.close
1394
1395     @sente.game = nil
1396     @gote.game = nil
1397     @sente.status = "connected"
1398     @gote.status = "connected"
1399
1400     if (@current_player.protocol == "CSA")
1401       @current_player.finish
1402     end
1403     if (@next_player.protocol == "CSA")
1404       @next_player.finish
1405     end
1406     @monitors = Array::new
1407     @sente = nil
1408     @gote = nil
1409     @current_player = nil
1410     @next_player = nil
1411     LEAGUE.games.delete(@id)
1412   end
1413
1414   def handle_one_move(str, player)
1415     finish_flag = true
1416     if (@current_player == player)
1417       @end_time = Time::new
1418       t = (@end_time - @start_time).floor
1419       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1420       
1421       move_status = nil
1422       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1423         status = :timeout
1424       elsif (str == :timeout)
1425         return false            # time isn't expired. players aren't swapped. continue game
1426       else
1427         @current_player.mytime = @current_player.mytime - t
1428         if (@current_player.mytime < 0)
1429           @current_player.mytime = 0
1430         end
1431
1432 #        begin
1433           move_status = @board.handle_one_move(str, @sente == @current_player)
1434 #        rescue
1435 #          log_error("handle_one_move raise exception for #{str}")
1436 #          move_status = :illegal
1437 #        end
1438
1439         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1440           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1441         else
1442           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1443             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1444             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1445             @fh.printf("%s\nT%d\n", str, t)
1446             @last_move = sprintf("%s,T%d", str, t)
1447             @current_turn = @current_turn + 1
1448           end
1449
1450           @monitors.each do |monitor|
1451             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1452             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1453           end
1454         end
1455       end
1456
1457       if (@next_player.status != "game") # rival is logout or disconnected
1458         abnormal_win()
1459       elsif (status == :timeout)
1460         timeout_lose()
1461       elsif (move_status == :illegal)
1462         illegal_lose()
1463       elsif (move_status == :kachi_win)
1464         kachi_win()
1465       elsif (move_status == :kachi_lose)
1466         kachi_lose()
1467       elsif (move_status == :toryo)
1468         toryo_lose()
1469       elsif (move_status == :outori)
1470         outori_win()
1471       elsif (move_status == :sennichite)
1472         sennichite_draw()
1473       elsif (move_status == :oute_sennichite)
1474         oute_sennichite_lose()
1475       elsif (move_status == :uchifuzume)
1476         uchifuzume_lose()
1477       elsif (move_status == :oute_kaihimore)
1478         oute_kaihimore_lose()
1479       else
1480         finish_flag = false
1481       end
1482       finish() if finish_flag
1483       (@current_player, @next_player) = [@next_player, @current_player]
1484       @start_time = Time::new
1485       return finish_flag
1486     end
1487   end
1488
1489   def abnormal_win
1490     @current_player.status = "connected"
1491     @next_player.status = "connected"
1492     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1493     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1494     @fh.printf("%%TORYO\n")
1495     @fh.print(@board.to_s.gsub(/^/, "\'"))
1496     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1497     @result = GameResultWin.new(@current_player, @next_player)
1498     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1499     @monitors.each do |monitor|
1500       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1501     end
1502   end
1503
1504   def abnormal_lose
1505     @current_player.status = "connected"
1506     @next_player.status = "connected"
1507     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1508     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1509     @fh.printf("%%TORYO\n")
1510     @fh.print(@board.to_s.gsub(/^/, "\'"))
1511     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1512     @result = GameResultWin.new(@next_player, @current_player)
1513     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1514     @monitors.each do |monitor|
1515       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1516     end
1517   end
1518
1519   def sennichite_draw
1520     @current_player.status = "connected"
1521     @next_player.status = "connected"
1522     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1523     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1524     @fh.print(@board.to_s.gsub(/^/, "\'"))
1525     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1526     @result = GameResultDraw.new(@current_player, @next_player)
1527     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1528     @monitors.each do |monitor|
1529       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1530     end
1531   end
1532
1533   def oute_sennichite_lose
1534     @current_player.status = "connected"
1535     @next_player.status = "connected"
1536     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1537     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1538     @fh.print(@board.to_s.gsub(/^/, "\'"))
1539     @fh.printf("'summary:oute_sennichite:%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] #OUTE_SENNICHITE\n", @id))
1544     end
1545   end
1546
1547   def illegal_lose
1548     @current_player.status = "connected"
1549     @next_player.status = "connected"
1550     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1551     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1552     @fh.print(@board.to_s.gsub(/^/, "\'"))
1553     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1554     @result = GameResultWin.new(@next_player, @current_player)
1555     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1556     @monitors.each do |monitor|
1557       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1558     end
1559   end
1560
1561   def uchifuzume_lose
1562     @current_player.status = "connected"
1563     @next_player.status = "connected"
1564     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1565     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1566     @fh.print(@board.to_s.gsub(/^/, "\'"))
1567     @fh.printf("'summary:uchifuzume:%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] #ILLEGAL_MOVE\n", @id))
1572     end
1573   end
1574
1575   def oute_kaihimore_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:oute_kaihimore:%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 timeout_lose
1590     @current_player.status = "connected"
1591     @next_player.status = "connected"
1592     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1593     @next_player.write_safe("#TIME_UP\n#WIN\n")
1594     @fh.print(@board.to_s.gsub(/^/, "\'"))
1595     @fh.printf("'summary:time up:%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] #TIME_UP\n", @id))
1600     end
1601   end
1602
1603   def kachi_win
1604     @current_player.status = "connected"
1605     @next_player.status = "connected"
1606     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1607     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1608     @fh.printf("%%KACHI\n")
1609     @fh.print(@board.to_s.gsub(/^/, "\'"))
1610     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1611     @result = GameResultWin.new(@current_player, @next_player)
1612     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1613     @monitors.each do |monitor|
1614       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1615     end
1616   end
1617
1618   def kachi_lose
1619     @current_player.status = "connected"
1620     @next_player.status = "connected"
1621     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1622     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1623     @fh.printf("%%KACHI\n")
1624     @fh.print(@board.to_s.gsub(/^/, "\'"))
1625     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1626     @result = GameResultWin.new(@next_player, @current_player)
1627     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1628     @monitors.each do |monitor|
1629       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1630     end
1631   end
1632
1633   def toryo_lose
1634     @current_player.status = "connected"
1635     @next_player.status = "connected"
1636     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1637     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1638     @fh.printf("%%TORYO\n")
1639     @fh.print(@board.to_s.gsub(/^/, "\'"))
1640     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1641     @result = GameResultWin.new(@next_player, @current_player)
1642     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1643     @monitors.each do |monitor|
1644       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1645     end
1646   end
1647
1648   def outori_win
1649     @current_player.status = "connected"
1650     @next_player.status = "connected"
1651     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1652     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1653     @fh.print(@board.to_s.gsub(/^/, "\'"))
1654     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1655     @result = GameResultWin.new(@current_player, @next_player)
1656     @fh.printf("'rating: #{@result.to_s}\n") if rated?
1657     @monitors.each do |monitor|
1658       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1659     end
1660   end
1661
1662   def start
1663     log_message(sprintf("game started %s", @id))
1664     @sente.write_safe(sprintf("START:%s\n", @id))
1665     @gote.write_safe(sprintf("START:%s\n", @id))
1666     @sente.mytime = @total_time
1667     @gote.mytime = @total_time
1668     @start_time = Time::new
1669   end
1670
1671   def propose
1672     begin
1673       @fh = open(@logfile, "w")
1674       @fh.sync = true
1675
1676       @fh.printf("V2\n")
1677       @fh.printf("N+%s\n", @sente.name)
1678       @fh.printf("N-%s\n", @gote.name)
1679       @fh.printf("$EVENT:%s\n", @id)
1680
1681       @sente.write_safe(propose_message("+"))
1682       @gote.write_safe(propose_message("-"))
1683
1684       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1685       @fh.print <<EOM
1686 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1687 P2 * -HI *  *  *  *  * -KA * 
1688 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1689 P4 *  *  *  *  *  *  *  *  * 
1690 P5 *  *  *  *  *  *  *  *  * 
1691 P6 *  *  *  *  *  *  *  *  * 
1692 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1693 P8 * +KA *  *  *  *  * +HI * 
1694 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1695 +
1696 EOM
1697     end
1698   end
1699
1700   def show()
1701     str0 = <<EOM
1702 BEGIN Game_Summary
1703 Protocol_Version:1.1
1704 Protocol_Mode:Server
1705 Format:Shogi 1.0
1706 Declaration:Jishogi 1.1
1707 Game_ID:#{@id}
1708 Name+:#{@sente.name}
1709 Name-:#{@gote.name}
1710 Rematch_On_Draw:NO
1711 To_Move:+
1712 BEGIN Time
1713 Time_Unit:1sec
1714 Total_Time:#{@total_time}
1715 Byoyomi:#{@byoyomi}
1716 Least_Time_Per_Move:#{Least_Time_Per_Move}
1717 Remaining_Time+:#{@sente.mytime}
1718 Remaining_Time-:#{@gote.mytime}
1719 Last_Move:#{@last_move}
1720 Current_Turn:#{@current_turn}
1721 END Time
1722 BEGIN Position
1723 EOM
1724
1725     str1 = <<EOM
1726 END Position
1727 END Game_Summary
1728 EOM
1729
1730     return str0 + @board.to_s + str1
1731   end
1732
1733   def propose_message(sg_flag)
1734     str = <<EOM
1735 BEGIN Game_Summary
1736 Protocol_Version:1.1
1737 Protocol_Mode:Server
1738 Format:Shogi 1.0
1739 Declaration:Jishogi 1.1
1740 Game_ID:#{@id}
1741 Name+:#{@sente.name}
1742 Name-:#{@gote.name}
1743 Your_Turn:#{sg_flag}
1744 Rematch_On_Draw:NO
1745 To_Move:+
1746 BEGIN Time
1747 Time_Unit:1sec
1748 Total_Time:#{@total_time}
1749 Byoyomi:#{@byoyomi}
1750 Least_Time_Per_Move:#{Least_Time_Per_Move}
1751 END Time
1752 BEGIN Position
1753 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1754 P2 * -HI *  *  *  *  * -KA * 
1755 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1756 P4 *  *  *  *  *  *  *  *  * 
1757 P5 *  *  *  *  *  *  *  *  * 
1758 P6 *  *  *  *  *  *  *  *  * 
1759 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1760 P8 * +KA *  *  *  *  * +HI * 
1761 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1762 P+
1763 P-
1764 +
1765 END Position
1766 END Game_Summary
1767 EOM
1768     return str
1769   end
1770   
1771   private
1772   
1773   def issue_current_time
1774     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1775     @@mutex.synchronize do
1776       while time <= @@time do
1777         time += 1
1778       end
1779       @@time = time
1780     end
1781   end
1782 end
1783
1784 #################################################
1785 # MAIN
1786 #
1787
1788 def usage
1789     print <<EOM
1790 NAME
1791         shogi-server - server for CSA server protocol
1792
1793 SYNOPSIS
1794         shogi-server event_name port_number
1795
1796 DESCRIPTION
1797         server for CSA server protocol
1798
1799 OPTIONS
1800         --pid-file file
1801                 specify filename for logging process ID
1802
1803 LICENSE
1804         this file is distributed under GPL version2 and might be compiled by Exerb
1805
1806 SEE ALSO
1807
1808 RELEASE
1809         #{Release}
1810
1811 REVISION
1812         #{Revision}
1813 EOM
1814 end
1815
1816 def log_message(str)
1817   printf("%s message: %s\n", Time::new.to_s, str)
1818 end
1819
1820 def log_warning(str)
1821   printf("%s warning: %s\n", Time::new.to_s, str)
1822 end
1823
1824 def log_error(str)
1825   printf("%s error: %s\n", Time::new.to_s, str)
1826 end
1827
1828
1829 def parse_command_line
1830   options = Hash::new
1831   parser = GetoptLong.new
1832   parser.ordering = GetoptLong::REQUIRE_ORDER
1833   parser.set_options(
1834                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1835
1836   parser.quiet = true
1837   begin
1838     parser.each_option do |name, arg|
1839       name.sub!(/^--/, '')
1840       options[name] = arg.dup
1841     end
1842   rescue
1843     usage
1844     raise parser.error_message
1845   end
1846   return options
1847 end
1848
1849 def good_game_name?(str)
1850   if ((str =~ /^(.+)-\d+-\d+$/) &&
1851       (good_identifier?($1)))
1852     return true
1853   else
1854     return false
1855   end
1856 end
1857
1858 def good_identifier?(str)
1859   if str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1860     return true
1861   elsif str =~ /\A[\w\d_@\-\.]{1,#{Max_Identifier_Length}},[\w\d_@\-\.]{1,#{Max_Identifier_Length}}\z/
1862     return true
1863   else
1864     return false
1865   end
1866 end
1867
1868 def good_login?(str)
1869   tokens = str.split
1870   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1871       (tokens[0] == "LOGIN") &&
1872       (good_identifier?(tokens[1])))
1873     return true
1874   else
1875     return false
1876   end
1877 end
1878
1879 def  write_pid_file(file)
1880   open(file, "w") do |fh|
1881     fh.print Process::pid, "\n"
1882   end
1883 end
1884
1885 def mutex_watchdog(mutex, sec)
1886   while true
1887     begin
1888       timeout(sec) do
1889         begin
1890           mutex.lock
1891         ensure
1892           mutex.unlock
1893         end
1894       end
1895       sleep(sec)
1896     rescue TimeoutError
1897       log_error("mutex watchdog timeout")
1898       exit(1)
1899     end
1900   end
1901 end
1902
1903 def main
1904   $mutex = Mutex::new
1905   Thread::start do
1906     Thread.pass
1907     mutex_watchdog($mutex, 10)
1908   end
1909
1910   $options = parse_command_line
1911   if (ARGV.length != 2)
1912     usage
1913     exit 2
1914   end
1915
1916   LEAGUE.event = ARGV.shift
1917   port = ARGV.shift
1918
1919   write_pid_file($options["pid-file"]) if ($options["pid-file"])
1920
1921   server = TCPserver.open(port)
1922   log_message("server started")
1923
1924   while true
1925     Thread::start(server.accept) do |client|
1926       Thread.pass
1927       client.sync = true
1928       player = nil
1929       while (str = client.gets_timeout(Login_Time))
1930         begin
1931           $mutex.lock
1932           str =~ /([\r\n]*)$/
1933           eol = $1
1934           if (good_login?(str))
1935             player = Player::new(str, client)
1936             if (LEAGUE.players[player.name])
1937               if ((LEAGUE.players[player.name].password == player.password) &&
1938                   (LEAGUE.players[player.name].status != "game"))
1939                 log_message(sprintf("user %s login forcely", player.name))
1940                 LEAGUE.players[player.name].kill
1941               else
1942                 client.write_safe("LOGIN:incorrect" + eol)
1943                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1944                 client.close
1945                 Thread::exit
1946                 return
1947               end
1948             end
1949             LEAGUE.add(player)
1950             break
1951           else
1952             client.write_safe("LOGIN:incorrect" + eol)
1953             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1954           end
1955         ensure
1956           $mutex.unlock
1957         end
1958       end                       # login loop
1959       if (! player)
1960         client.close
1961         Thread::exit
1962         return
1963       end
1964       log_message(sprintf("user %s login", player.name))
1965       player.run
1966       begin
1967         $mutex.lock
1968         if (player.game)
1969           player.game.kill(player)
1970         end
1971         player.finish # socket has been closed
1972         LEAGUE.delete(player)
1973         log_message(sprintf("user %s logout", player.name))
1974       ensure
1975         $mutex.unlock
1976       end
1977     end
1978   end
1979 end
1980
1981 if ($0 == __FILE__)
1982   LEAGUE = League::new
1983   main
1984 end