OSDN Git Service

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