OSDN Git Service

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