OSDN Git Service

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