OSDN Git Service

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