OSDN Git Service

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