OSDN Git Service

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