OSDN Git Service

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