OSDN Git Service

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