OSDN Git Service

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