OSDN Git Service

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