OSDN Git Service

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