OSDN Git Service

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