OSDN Git Service

Refactoring. Pretty output.
[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 module ShogiServer # for a namespace
21
22 Max_Write_Queue_Size = 1000
23 Max_Identifier_Length = 32
24 Default_Timeout = 60            # for single socket operation
25
26 Default_Game_Name = "default-1500-0"
27
28 One_Time = 10
29 Least_Time_Per_Move = 1
30 Login_Time = 300                # time for LOGIN
31
32 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
33 Release.concat("-") if (Release == "")
34 Revision = "$Revision$".gsub(/[^\.\d]/, '')
35
36 require 'getoptlong'
37 require 'thread'
38 require 'timeout'
39 require 'socket'
40 require 'yaml'
41 require 'yaml/store'
42 require 'digest/md5'
43
44
45
46 class TCPSocket
47   def gets_timeout(t = Default_Timeout)
48     begin
49       timeout(t) do
50         return self.gets
51       end
52     rescue TimeoutError
53       return nil
54     rescue
55       return nil
56     end
57   end
58   def gets_safe(t = nil)
59     if (t && t > 0)
60       begin
61         timeout(t) do
62           return self.gets
63         end
64       rescue TimeoutError
65         return :timeout
66       rescue
67         return nil
68       end
69     else
70       begin
71         return self.gets
72       rescue
73         return nil
74       end
75     end
76   end
77   def write_safe(str)
78     begin
79       return self.write(str)
80     rescue
81       return nil
82     end
83   end
84 end
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   def handle_one_move(str, sente=nil)
1275     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
1276       sg = $1
1277       x0 = $2.to_i
1278       y0 = $3.to_i
1279       x1 = $4.to_i
1280       y1 = $5.to_i
1281       name = $6
1282     elsif (str =~ /^%KACHI/)
1283       raise ArgumentError, "sente is null", caller if sente == nil
1284       if (good_kachi?(sente))
1285         return :kachi_win
1286       else
1287         return :kachi_lose
1288       end
1289     elsif (str =~ /^%TORYO/)
1290       return :toryo
1291     else
1292       return :illegal
1293     end
1294     
1295     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
1296         ((x0 != 0) || (y0 != 0)))
1297       return :illegal
1298     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
1299       return :illegal
1300     end
1301     
1302     if (sg == "+")
1303       sente = true
1304       hands = @sente_hands
1305     else
1306       sente = false
1307       hands = @gote_hands
1308     end
1309     
1310     ## source check
1311     if ((x0 == 0) && (y0 == 0))
1312       return :illegal if (! have_piece?(hands, name))
1313     elsif (! @array[x0][y0])
1314       return :illegal           # no piece
1315     elsif (@array[x0][y0].sente != sente)
1316       return :illegal           # this is not mine
1317     elsif (@array[x0][y0].name != name)
1318       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1319     end
1320
1321     ## destination check
1322     if (@array[x1][y1] &&
1323         (@array[x1][y1].sente == sente)) # can't capture mine
1324       return :illegal
1325     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1326       return :illegal           # can't put on existing piece
1327     end
1328
1329     tmp_board = Marshal.load(Marshal.dump(self))
1330     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1331     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1332     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1333     return :sennichite if tmp_board.sennichite?(sente)
1334
1335     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1336       return :uchifuzume
1337     end
1338
1339     move_to(x0, y0, x1, y1, name, sente)
1340     str = to_s
1341
1342     if (checkmated?(! sente))
1343       if (sente)
1344         @sente_history[str] = (@sente_history[str] || 0) + 1
1345       else
1346         @gote_history[str] = (@gote_history[str] || 0) + 1
1347       end
1348     else
1349       if (sente)
1350         @sente_history.clear
1351       else
1352         @gote_history.clear
1353       end
1354     end
1355     @history[str] = (@history[str] || 0) + 1
1356     return :normal
1357   end
1358
1359   def to_s
1360     a = Array::new
1361     y = 1
1362     while (y <= 9)
1363       a.push(sprintf("P%d", y))
1364       x = 9
1365       while (x >= 1)
1366         piece = @array[x][y]
1367         if (piece)
1368           s = piece.to_s
1369         else
1370           s = " * "
1371         end
1372         a.push(s)
1373         x = x - 1
1374       end
1375       a.push(sprintf("\n"))
1376       y = y + 1
1377     end
1378     if (! sente_hands.empty?)
1379       a.push("P+")
1380       sente_hands.each do |p|
1381         a.push("00" + p.name)
1382       end
1383       a.push("\n")
1384     end
1385     if (! gote_hands.empty?)
1386       a.push("P-")
1387       gote_hands.each do |p|
1388         a.push("00" + p.name)
1389       end
1390       a.push("\n")
1391     end
1392     a.push("+\n")
1393     return a.join
1394   end
1395 end
1396
1397 class GameResult
1398   attr_reader :players, :black, :white
1399
1400   def initialize(p1, p2)
1401     @players = []
1402     @players << p1
1403     @players << p2
1404     if p1.sente && !p2.sente
1405       @black, @white = p1, p2
1406     elsif !p1.sente && p2.sente
1407       @black, @white = p2, p1
1408     else
1409       raise "Never reached!"
1410     end
1411   end
1412 end
1413
1414 class GameResultWin < GameResult
1415   attr_reader :winner, :loser
1416
1417   def initialize(winner, loser)
1418     super
1419     @winner, @loser = winner, loser
1420   end
1421
1422   def to_s
1423     black_name = @black.id || @black.name
1424     white_name = @white.id || @white.name
1425     "%s:%s" % [black_name, white_name]
1426   end
1427 end
1428
1429 class GameResultDraw < GameResult
1430
1431 end
1432
1433 class Game
1434   @@mutex = Mutex.new
1435   @@time  = 0
1436
1437   def initialize(game_name, player0, player1)
1438     @monitors = Array::new
1439     @game_name = game_name
1440     if (@game_name =~ /-(\d+)-(\d+)$/)
1441       @total_time = $1.to_i
1442       @byoyomi = $2.to_i
1443     end
1444
1445     if (player0.sente)
1446       @sente = player0
1447       @gote = player1
1448     else
1449       @sente = player1
1450       @gote = player0
1451     end
1452     @current_player = @sente
1453     @next_player = @gote
1454
1455     @sente.game = self
1456     @gote.game = self
1457
1458     @last_move = ""
1459     @current_turn = 0
1460
1461     @sente.status = "agree_waiting"
1462     @gote.status = "agree_waiting"
1463     
1464     @id = sprintf("%s+%s+%s+%s+%s", 
1465                   LEAGUE.event, @game_name, @sente.name, @gote.name, issue_current_time)
1466     @logfile = @id + ".csa"
1467
1468     LEAGUE.games[@id] = self
1469
1470     log_message(sprintf("game created %s", @id))
1471
1472     @board = Board::new
1473     @board.initial
1474     @start_time = nil
1475     @fh = nil
1476     @result = nil
1477
1478     propose
1479   end
1480   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1481   attr_accessor :last_move, :current_turn
1482   attr_reader   :result
1483
1484   def rated?
1485     @sente.rated? && @gote.rated?
1486   end
1487
1488   def monitoron(monitor)
1489     @monitors.delete(monitor)
1490     @monitors.push(monitor)
1491   end
1492
1493   def monitoroff(monitor)
1494     @monitors.delete(monitor)
1495   end
1496
1497   def reject(rejector)
1498     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1499     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1500     finish
1501   end
1502
1503   def kill(killer)
1504     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1505       reject(killer.name)
1506     elsif (@current_player == killer)
1507       abnormal_lose()
1508       finish
1509     end
1510   end
1511
1512   def finish
1513     log_message(sprintf("game finished %s", @id))
1514     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1515     @fh.close
1516
1517     @sente.game = nil
1518     @gote.game = nil
1519     @sente.status = "connected"
1520     @gote.status = "connected"
1521
1522     if (@current_player.protocol == LoginCSA::PROTOCOL)
1523       @current_player.finish
1524     end
1525     if (@next_player.protocol == LoginCSA::PROTOCOL)
1526       @next_player.finish
1527     end
1528     @monitors = Array::new
1529     @sente = nil
1530     @gote = nil
1531     @current_player = nil
1532     @next_player = nil
1533     LEAGUE.games.delete(@id)
1534   end
1535
1536   def handle_one_move(str, player)
1537     finish_flag = true
1538     if (@current_player == player)
1539       @end_time = Time::new
1540       t = (@end_time - @start_time).floor
1541       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1542       
1543       move_status = nil
1544       if ((@current_player.mytime - t <= -@byoyomi) && ((@total_time > 0) || (@byoyomi > 0)))
1545         status = :timeout
1546       elsif (str == :timeout)
1547         return false            # time isn't expired. players aren't swapped. continue game
1548       else
1549         @current_player.mytime = @current_player.mytime - t
1550         if (@current_player.mytime < 0)
1551           @current_player.mytime = 0
1552         end
1553
1554 #        begin
1555           move_status = @board.handle_one_move(str, @sente == @current_player)
1556 #        rescue
1557 #          log_error("handle_one_move raise exception for #{str}")
1558 #          move_status = :illegal
1559 #        end
1560
1561         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1562           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1563         else
1564           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1565             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1566             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1567             @fh.printf("%s\nT%d\n", str, t)
1568             @last_move = sprintf("%s,T%d", str, t)
1569             @current_turn = @current_turn + 1
1570           end
1571
1572           @monitors.each do |monitor|
1573             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1574             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1575           end
1576         end
1577       end
1578
1579       if (@next_player.status != "game") # rival is logout or disconnected
1580         abnormal_win()
1581       elsif (status == :timeout)
1582         timeout_lose()
1583       elsif (move_status == :illegal)
1584         illegal_lose()
1585       elsif (move_status == :kachi_win)
1586         kachi_win()
1587       elsif (move_status == :kachi_lose)
1588         kachi_lose()
1589       elsif (move_status == :toryo)
1590         toryo_lose()
1591       elsif (move_status == :outori)
1592         outori_win()
1593       elsif (move_status == :sennichite)
1594         sennichite_draw()
1595       elsif (move_status == :oute_sennichite)
1596         oute_sennichite_lose()
1597       elsif (move_status == :uchifuzume)
1598         uchifuzume_lose()
1599       elsif (move_status == :oute_kaihimore)
1600         oute_kaihimore_lose()
1601       else
1602         finish_flag = false
1603       end
1604       finish() if finish_flag
1605       (@current_player, @next_player) = [@next_player, @current_player]
1606       @start_time = Time::new
1607       return finish_flag
1608     end
1609   end
1610
1611   def abnormal_win
1612     @current_player.status = "connected"
1613     @next_player.status = "connected"
1614     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1615     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1616     @fh.printf("%%TORYO\n")
1617     @fh.print(@board.to_s.gsub(/^/, "\'"))
1618     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1619     @result = GameResultWin.new(@current_player, @next_player)
1620     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1621     @monitors.each do |monitor|
1622       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1623     end
1624   end
1625
1626   def abnormal_lose
1627     @current_player.status = "connected"
1628     @next_player.status = "connected"
1629     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1630     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1631     @fh.printf("%%TORYO\n")
1632     @fh.print(@board.to_s.gsub(/^/, "\'"))
1633     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1634     @result = GameResultWin.new(@next_player, @current_player)
1635     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1636     @monitors.each do |monitor|
1637       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1638     end
1639   end
1640
1641   def sennichite_draw
1642     @current_player.status = "connected"
1643     @next_player.status = "connected"
1644     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1645     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1646     @fh.print(@board.to_s.gsub(/^/, "\'"))
1647     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1648     @result = GameResultDraw.new(@current_player, @next_player)
1649     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1650     @monitors.each do |monitor|
1651       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1652     end
1653   end
1654
1655   def oute_sennichite_lose
1656     @current_player.status = "connected"
1657     @next_player.status = "connected"
1658     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1659     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1660     @fh.print(@board.to_s.gsub(/^/, "\'"))
1661     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1662     @result = GameResultWin.new(@next_player, @current_player)
1663     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1664     @monitors.each do |monitor|
1665       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1666     end
1667   end
1668
1669   def illegal_lose
1670     @current_player.status = "connected"
1671     @next_player.status = "connected"
1672     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1673     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1674     @fh.print(@board.to_s.gsub(/^/, "\'"))
1675     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1676     @result = GameResultWin.new(@next_player, @current_player)
1677     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1678     @monitors.each do |monitor|
1679       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1680     end
1681   end
1682
1683   def uchifuzume_lose
1684     @current_player.status = "connected"
1685     @next_player.status = "connected"
1686     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1687     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1688     @fh.print(@board.to_s.gsub(/^/, "\'"))
1689     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1690     @result = GameResultWin.new(@next_player, @current_player)
1691     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1692     @monitors.each do |monitor|
1693       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1694     end
1695   end
1696
1697   def oute_kaihimore_lose
1698     @current_player.status = "connected"
1699     @next_player.status = "connected"
1700     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1701     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1702     @fh.print(@board.to_s.gsub(/^/, "\'"))
1703     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1704     @result = GameResultWin.new(@next_player, @current_player)
1705     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1706     @monitors.each do |monitor|
1707       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1708     end
1709   end
1710
1711   def timeout_lose
1712     @current_player.status = "connected"
1713     @next_player.status = "connected"
1714     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1715     @next_player.write_safe("#TIME_UP\n#WIN\n")
1716     @fh.print(@board.to_s.gsub(/^/, "\'"))
1717     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1718     @result = GameResultWin.new(@next_player, @current_player)
1719     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1720     @monitors.each do |monitor|
1721       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1722     end
1723   end
1724
1725   def kachi_win
1726     @current_player.status = "connected"
1727     @next_player.status = "connected"
1728     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1729     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1730     @fh.printf("%%KACHI\n")
1731     @fh.print(@board.to_s.gsub(/^/, "\'"))
1732     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1733     @result = GameResultWin.new(@current_player, @next_player)
1734     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1735     @monitors.each do |monitor|
1736       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1737     end
1738   end
1739
1740   def kachi_lose
1741     @current_player.status = "connected"
1742     @next_player.status = "connected"
1743     @current_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#LOSE\n")
1744     @next_player.write_safe("%KACHI\n#ILLEGAL_MOVE\n#WIN\n")
1745     @fh.printf("%%KACHI\n")
1746     @fh.print(@board.to_s.gsub(/^/, "\'"))
1747     @fh.printf("'summary:illegal kachi:%s lose:%s win\n", @current_player.name, @next_player.name)
1748     @result = GameResultWin.new(@next_player, @current_player)
1749     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1750     @monitors.each do |monitor|
1751       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1752     end
1753   end
1754
1755   def toryo_lose
1756     @current_player.status = "connected"
1757     @next_player.status = "connected"
1758     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1759     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1760     @fh.printf("%%TORYO\n")
1761     @fh.print(@board.to_s.gsub(/^/, "\'"))
1762     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1763     @result = GameResultWin.new(@next_player, @current_player)
1764     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1765     @monitors.each do |monitor|
1766       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1767     end
1768   end
1769
1770   def outori_win
1771     @current_player.status = "connected"
1772     @next_player.status = "connected"
1773     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1774     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1775     @fh.print(@board.to_s.gsub(/^/, "\'"))
1776     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1777     @result = GameResultWin.new(@current_player, @next_player)
1778     @fh.printf("'rating:#{@result.to_s}\n") if rated?
1779     @monitors.each do |monitor|
1780       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1781     end
1782   end
1783
1784   def start
1785     log_message(sprintf("game started %s", @id))
1786     @sente.write_safe(sprintf("START:%s\n", @id))
1787     @gote.write_safe(sprintf("START:%s\n", @id))
1788     @sente.mytime = @total_time
1789     @gote.mytime = @total_time
1790     @start_time = Time::new
1791   end
1792
1793   def propose
1794     begin
1795       @fh = open(@logfile, "w")
1796       @fh.sync = true
1797
1798       @fh.printf("V2\n")
1799       @fh.printf("N+%s\n", @sente.name)
1800       @fh.printf("N-%s\n", @gote.name)
1801       @fh.printf("$EVENT:%s\n", @id)
1802
1803       @sente.write_safe(propose_message("+"))
1804       @gote.write_safe(propose_message("-"))
1805
1806       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1807       @fh.print <<EOM
1808 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1809 P2 * -HI *  *  *  *  * -KA * 
1810 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1811 P4 *  *  *  *  *  *  *  *  * 
1812 P5 *  *  *  *  *  *  *  *  * 
1813 P6 *  *  *  *  *  *  *  *  * 
1814 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1815 P8 * +KA *  *  *  *  * +HI * 
1816 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1817 +
1818 EOM
1819     end
1820   end
1821
1822   def show()
1823     str0 = <<EOM
1824 BEGIN Game_Summary
1825 Protocol_Version:1.1
1826 Protocol_Mode:Server
1827 Format:Shogi 1.0
1828 Declaration:Jishogi 1.1
1829 Game_ID:#{@id}
1830 Name+:#{@sente.name}
1831 Name-:#{@gote.name}
1832 Rematch_On_Draw:NO
1833 To_Move:+
1834 BEGIN Time
1835 Time_Unit:1sec
1836 Total_Time:#{@total_time}
1837 Byoyomi:#{@byoyomi}
1838 Least_Time_Per_Move:#{Least_Time_Per_Move}
1839 Remaining_Time+:#{@sente.mytime}
1840 Remaining_Time-:#{@gote.mytime}
1841 Last_Move:#{@last_move}
1842 Current_Turn:#{@current_turn}
1843 END Time
1844 BEGIN Position
1845 EOM
1846
1847     str1 = <<EOM
1848 END Position
1849 END Game_Summary
1850 EOM
1851
1852     return str0 + @board.to_s + str1
1853   end
1854
1855   def propose_message(sg_flag)
1856     str = <<EOM
1857 BEGIN Game_Summary
1858 Protocol_Version:1.1
1859 Protocol_Mode:Server
1860 Format:Shogi 1.0
1861 Declaration:Jishogi 1.1
1862 Game_ID:#{@id}
1863 Name+:#{@sente.name}
1864 Name-:#{@gote.name}
1865 Your_Turn:#{sg_flag}
1866 Rematch_On_Draw:NO
1867 To_Move:+
1868 BEGIN Time
1869 Time_Unit:1sec
1870 Total_Time:#{@total_time}
1871 Byoyomi:#{@byoyomi}
1872 Least_Time_Per_Move:#{Least_Time_Per_Move}
1873 END Time
1874 BEGIN Position
1875 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1876 P2 * -HI *  *  *  *  * -KA * 
1877 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1878 P4 *  *  *  *  *  *  *  *  * 
1879 P5 *  *  *  *  *  *  *  *  * 
1880 P6 *  *  *  *  *  *  *  *  * 
1881 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1882 P8 * +KA *  *  *  *  * +HI * 
1883 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1884 P+
1885 P-
1886 +
1887 END Position
1888 END Game_Summary
1889 EOM
1890     return str
1891   end
1892   
1893   private
1894   
1895   def issue_current_time
1896     time = Time::new.strftime("%Y%m%d%H%M%S").to_i
1897     @@mutex.synchronize do
1898       while time <= @@time do
1899         time += 1
1900       end
1901       @@time = time
1902     end
1903   end
1904 end
1905
1906 #################################################
1907 # MAIN
1908 #
1909
1910 def usage
1911     print <<EOM
1912 NAME
1913         shogi-server - server for CSA server protocol
1914
1915 SYNOPSIS
1916         shogi-server event_name port_number
1917
1918 DESCRIPTION
1919         server for CSA server protocol
1920
1921 OPTIONS
1922         --pid-file file
1923                 specify filename for logging process ID
1924
1925 LICENSE
1926         this file is distributed under GPL version2 and might be compiled by Exerb
1927
1928 SEE ALSO
1929
1930 RELEASE
1931         #{Release}
1932
1933 REVISION
1934         #{Revision}
1935 EOM
1936 end
1937
1938 def log_message(str)
1939   printf("%s message: %s\n", Time::new.to_s, str)
1940 end
1941
1942 def log_warning(str)
1943   printf("%s warning: %s\n", Time::new.to_s, str)
1944 end
1945
1946 def log_error(str)
1947   printf("%s error: %s\n", Time::new.to_s, str)
1948 end
1949
1950
1951 def parse_command_line
1952   options = Hash::new
1953   parser = GetoptLong.new
1954   parser.ordering = GetoptLong::REQUIRE_ORDER
1955   parser.set_options(
1956                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1957
1958   parser.quiet = true
1959   begin
1960     parser.each_option do |name, arg|
1961       name.sub!(/^--/, '')
1962       options[name] = arg.dup
1963     end
1964   rescue
1965     usage
1966     raise parser.error_message
1967   end
1968   return options
1969 end
1970
1971 def write_pid_file(file)
1972   open(file, "w") do |fh|
1973     fh.print Process::pid, "\n"
1974   end
1975 end
1976
1977 def mutex_watchdog(mutex, sec)
1978   while true
1979     begin
1980       timeout(sec) do
1981         begin
1982           mutex.lock
1983         ensure
1984           mutex.unlock
1985         end
1986       end
1987       sleep(sec)
1988     rescue TimeoutError
1989       log_error("mutex watchdog timeout")
1990       exit(1)
1991     end
1992   end
1993 end
1994
1995 def main
1996
1997   $mutex = Mutex::new
1998   Thread::start do
1999     Thread.pass
2000     mutex_watchdog($mutex, 10)
2001   end
2002
2003   $options = parse_command_line
2004   if (ARGV.length != 2)
2005     usage
2006     exit 2
2007   end
2008
2009   LEAGUE.event = ARGV.shift
2010   port = ARGV.shift
2011
2012   write_pid_file($options["pid-file"]) if ($options["pid-file"])
2013
2014   server = TCPserver.open(port)
2015   log_message("server started")
2016
2017   while true
2018     Thread::start(server.accept) do |client|
2019       Thread.pass
2020       client.sync = true
2021       player = nil
2022       login  = nil
2023       while (str = client.gets_timeout(Login_Time))
2024         begin
2025           $mutex.lock
2026           str =~ /([\r\n]*)$/
2027           eol = $1
2028           if (Login::good_login?(str))
2029             player = Player::new(str, client)
2030             player.eol = eol
2031             login  = Login::factory(str, player)
2032             if (LEAGUE.players[player.name])
2033               if ((LEAGUE.players[player.name].password == player.password) &&
2034                   (LEAGUE.players[player.name].status != "game"))
2035                 log_message(sprintf("user %s login forcely", player.name))
2036                 LEAGUE.players[player.name].kill
2037               else
2038                 login.incorrect_duplicated_player(str)
2039                 Thread::exit
2040                 return
2041               end
2042             end
2043             LEAGUE.add(player)
2044             break
2045           else
2046             client.write_safe("LOGIN:incorrect" + eol)
2047             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
2048           end
2049         ensure
2050           $mutex.unlock
2051         end
2052       end                       # login loop
2053       if (! player)
2054         client.close
2055         Thread::exit
2056         return
2057       end
2058       log_message(sprintf("user %s login", player.name))
2059       login.process
2060       player.run(login.csa_1st_str)
2061       begin
2062         $mutex.lock
2063         if (player.game)
2064           player.game.kill(player)
2065         end
2066         player.finish # socket has been closed
2067         LEAGUE.delete(player)
2068         log_message(sprintf("user %s logout", player.name))
2069       ensure
2070         $mutex.unlock
2071       end
2072     end
2073   end
2074 end
2075 module_function :main
2076
2077 end # module ShogiServer
2078
2079 if ($0 == __FILE__)
2080   STDOUT.sync = true
2081   STDERR.sync = true
2082   TCPSocket.do_not_reverse_lookup = true
2083   Thread.abort_on_exception = true
2084   
2085   LEAGUE = ShogiServer::League::new
2086   ShogiServer::main
2087 end