OSDN Git Service

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