OSDN Git Service

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