OSDN Git Service

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