OSDN Git Service

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