OSDN Git Service

show last board
[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       if (@sente)
499         @board.sente_hands.push(self)
500       else
501         @board.gote_hands.push(self)
502       end
503       @board.array[@x][@y] = nil
504     else
505       @board.array[@x][@y] = nil
506       @board.array[x][y] = self
507     end
508     @x = x
509     @y = y
510   end
511
512   def name
513     @name
514   end
515
516   def promoted_name
517     @promoted_name
518   end
519
520   def to_s
521     if (@sente)
522       sg = "+"
523     else
524       sg = "-"
525     end
526     if (@promoted)
527       n = @promoted_name
528     else
529       n = @name
530     end
531     return sg + n
532   end
533 end
534
535 class PieceFU < Piece
536   def initialize(*arg)
537     @normal_moves = [[0, +1]]
538     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
539     @name = "FU"
540     @promoted_name = "TO"
541     super
542   end
543   def room_of_head?(x, y, name)
544     if (name == "FU")
545       if (@sente)
546         return false if (y == 1)
547       else
548         return false if (y == 9)
549       end
550       ## 2fu check
551       c = 0
552       iy = 1
553       while (iy <= 9)
554         if ((iy  != @y) &&      # not source position
555             @board.array[x][iy] &&
556             (@board.array[x][iy].sente == @sente) && # mine
557             (@board.array[x][iy].name == "FU") &&
558             (@board.array[x][iy].promoted == false))
559           return false
560         end
561         iy = iy + 1
562       end
563     end
564     return true
565   end
566 end
567
568 class PieceKY  < Piece
569   def initialize(*arg)
570     @normal_moves = []
571     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
572     @name = "KY"
573     @promoted_name = "NY"
574     super
575   end
576   def room_of_head?(x, y, name)
577     if (name == "KY")
578       if (@sente)
579         return false if (y == 1)
580       else
581         return false if (y == 9)
582       end
583     end
584     return true
585   end
586   def far_movable_grids
587     grids = Array::new
588     if (@promoted)
589       return []
590     else
591       if (@sente)                 # up
592         cand_x = @x
593         cand_y = @y - 1
594         while (jump_to?(cand_x, cand_y))
595           grids.push([cand_x, cand_y])
596           break if (! put_to?(cand_x, cand_y))
597           cand_y = cand_y - 1
598         end
599       else                        # down
600         cand_x = @x
601         cand_y = @y + 1
602         while (jump_to?(cand_x, cand_y))
603           grids.push([cand_x, cand_y])
604           break if (! put_to?(cand_x, cand_y))
605           cand_y = cand_y + 1
606         end
607       end
608       return grids
609     end
610   end
611 end
612 class PieceKE  < Piece
613   def initialize(*arg)
614     @normal_moves = [[+1, +2], [-1, +2]]
615     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
616     @name = "KE"
617     @promoted_name = "NK"
618     super
619   end
620   def room_of_head?(x, y, name)
621     if (name == "KE")
622       if (@sente)
623         return false if ((y == 1) || (y == 2))
624       else
625         return false if ((y == 9) || (y == 8))
626       end
627     end
628     return true
629   end
630 end
631 class PieceGI  < Piece
632   def initialize(*arg)
633     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, -1], [-1, -1]]
634     @promoted_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
635     @name = "GI"
636     @promoted_name = "NG"
637     super
638   end
639 end
640 class PieceKI  < Piece
641   def initialize(*arg)
642     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1]]
643     @promoted_moves = []
644     @name = "KI"
645     @promoted_name = nil
646     super
647   end
648 end
649 class PieceKA  < Piece
650   def initialize(*arg)
651     @normal_moves = []
652     @promoted_moves = [[0, +1], [+1, 0], [-1, 0], [0, -1]]
653     @name = "KA"
654     @promoted_name = "UM"
655     super
656   end
657   def far_movable_grids
658     grids = Array::new
659     ## up right
660     cand_x = @x - 1
661     cand_y = @y - 1
662     while (jump_to?(cand_x, cand_y))
663       grids.push([cand_x, cand_y])
664       break if (! put_to?(cand_x, cand_y))
665       cand_x = cand_x - 1
666       cand_y = cand_y - 1
667     end
668     ## down right
669     cand_x = @x - 1
670     cand_y = @y + 1
671     while (jump_to?(cand_x, cand_y))
672       grids.push([cand_x, cand_y])
673       break if (! put_to?(cand_x, cand_y))
674       cand_x = cand_x - 1
675       cand_y = cand_y + 1
676     end
677     ## up left
678     cand_x = @x + 1
679     cand_y = @y - 1
680     while (jump_to?(cand_x, cand_y))
681       grids.push([cand_x, cand_y])
682       break if (! put_to?(cand_x, cand_y))
683       cand_x = cand_x + 1
684       cand_y = cand_y - 1
685     end
686     ## down left
687     cand_x = @x + 1
688     cand_y = @y + 1
689     while (jump_to?(cand_x, cand_y))
690       grids.push([cand_x, cand_y])
691       break if (! put_to?(cand_x, cand_y))
692       cand_x = cand_x + 1
693       cand_y = cand_y + 1
694     end
695     return grids
696   end
697 end
698 class PieceHI  < Piece
699   def initialize(*arg)
700     @normal_moves = []
701     @promoted_moves = [[+1, +1], [-1, +1], [+1, -1], [-1, -1]]
702     @name = "HI"
703     @promoted_name = "RY"
704     super
705   end
706   def far_movable_grids
707     grids = Array::new
708     ## up
709     cand_x = @x
710     cand_y = @y - 1
711     while (jump_to?(cand_x, cand_y))
712       grids.push([cand_x, cand_y])
713       break if (! put_to?(cand_x, cand_y))
714       cand_y = cand_y - 1
715     end
716     ## down
717     cand_x = @x
718     cand_y = @y + 1
719     while (jump_to?(cand_x, cand_y))
720       grids.push([cand_x, cand_y])
721       break if (! put_to?(cand_x, cand_y))
722       cand_y = cand_y + 1
723     end
724     ## right
725     cand_x = @x - 1
726     cand_y = @y
727     while (jump_to?(cand_x, cand_y))
728       grids.push([cand_x, cand_y])
729       break if (! put_to?(cand_x, cand_y))
730       cand_x = cand_x - 1
731     end
732     ## down
733     cand_x = @x + 1
734     cand_y = @y
735     while (jump_to?(cand_x, cand_y))
736       grids.push([cand_x, cand_y])
737       break if (! put_to?(cand_x, cand_y))
738       cand_x = cand_x + 1
739     end
740     return grids
741   end
742 end
743 class PieceOU < Piece
744   def initialize(*arg)
745     @normal_moves = [[0, +1], [+1, +1], [-1, +1], [+1, +0], [-1, +0], [0, -1], [+1, -1], [-1, -1]]
746     @promoted_moves = []
747     @name = "OU"
748     @promoted_name = nil
749     super
750   end
751 end
752
753 class Board
754   def initialize
755     @sente_hands = Array::new
756     @gote_hands = Array::new
757     @history = Hash::new
758     @sente_history = Hash::new
759     @gote_history = Hash::new
760     @array = [[], [], [], [], [], [], [], [], [], []]
761   end
762   attr_accessor :array, :sente_hands, :gote_hands, :history, :sente_history, :gote_history
763
764   def initial
765     PieceKY::new(self, 1, 1, false)
766     PieceKE::new(self, 2, 1, false)
767     PieceGI::new(self, 3, 1, false)
768     PieceKI::new(self, 4, 1, false)
769     PieceOU::new(self, 5, 1, false)
770     PieceKI::new(self, 6, 1, false)
771     PieceGI::new(self, 7, 1, false)
772     PieceKE::new(self, 8, 1, false)
773     PieceKY::new(self, 9, 1, false)
774     PieceKA::new(self, 2, 2, false)
775     PieceHI::new(self, 8, 2, false)
776     PieceFU::new(self, 1, 3, false)
777     PieceFU::new(self, 2, 3, false)
778     PieceFU::new(self, 3, 3, false)
779     PieceFU::new(self, 4, 3, false)
780     PieceFU::new(self, 5, 3, false)
781     PieceFU::new(self, 6, 3, false)
782     PieceFU::new(self, 7, 3, false)
783     PieceFU::new(self, 8, 3, false)
784     PieceFU::new(self, 9, 3, false)
785
786     PieceKY::new(self, 1, 9, true)
787     PieceKE::new(self, 2, 9, true)
788     PieceGI::new(self, 3, 9, true)
789     PieceKI::new(self, 4, 9, true)
790     PieceOU::new(self, 5, 9, true)
791     PieceKI::new(self, 6, 9, true)
792     PieceGI::new(self, 7, 9, true)
793     PieceKE::new(self, 8, 9, true)
794     PieceKY::new(self, 9, 9, true)
795     PieceKA::new(self, 8, 8, true)
796     PieceHI::new(self, 2, 8, true)
797     PieceFU::new(self, 1, 7, true)
798     PieceFU::new(self, 2, 7, true)
799     PieceFU::new(self, 3, 7, true)
800     PieceFU::new(self, 4, 7, true)
801     PieceFU::new(self, 5, 7, true)
802     PieceFU::new(self, 6, 7, true)
803     PieceFU::new(self, 7, 7, true)
804     PieceFU::new(self, 8, 7, true)
805     PieceFU::new(self, 9, 7, true)
806   end
807
808   def have_piece?(hands, name)
809     piece = hands.find { |i|
810       i.name == name
811     }
812     return piece
813   end
814
815   def move_to(x0, y0, x1, y1, name, sente)
816     if (sente)
817       hands = @sente_hands
818     else
819       hands = @gote_hands
820     end
821
822     if ((x0 == 0) || (y0 == 0))
823       piece = have_piece?(hands, name)
824       return :illegal if (! piece.move_to?(x1, y1, name))
825       piece.move_to(x1, y1)
826       piece.promoted = false
827     else
828       return :illegal if (! @array[x0][y0].move_to?(x1, y1, name))
829       if (@array[x0][y0].name != name) # promoted ?
830         @array[x0][y0].promoted = true
831       end
832       if (@array[x1][y1])
833         if (@array[x1][y1].name == "OU")
834           return :outori        # return board update
835         end
836         @array[x1][y1].sente = @array[x0][y0].sente
837         @array[x1][y1].move_to(0, 0)
838         hands.sort! {|a, b|
839           a.name <=> b.name
840         }
841       end
842       @array[x0][y0].move_to(x1, y1)
843     end
844     return true
845   end
846
847   def look_for_ou(sente)
848     x = 1
849     while (x <= 9)
850       y = 1
851       while (y <= 9)
852         if (@array[x][y] &&
853             (@array[x][y].name == "OU") &&
854             (@array[x][y].sente == sente))
855           return @array[x][y]
856         end
857         y = y + 1
858       end
859       x = x + 1
860     end
861     raise "can't find ou"
862   end
863
864   def checkmated?(sente)        # sente is loosing
865     ou = look_for_ou(sente)
866     x = 1
867     while (x <= 9)
868       y = 1
869       while (y <= 9)
870         if (@array[x][y] &&
871             (@array[x][y].sente != sente))
872           if (@array[x][y].movable_grids.include?([ou.x, ou.y]))
873             return true
874           end
875         end
876         y = y + 1
877       end
878       x = x + 1
879     end
880     return false
881   end
882
883   def uchifuzume?(sente)
884     rival_ou = look_for_ou(! sente)   # rival's ou
885     if (sente)                  # rival is gote
886       if ((rival_ou.y != 9) &&
887           (@array[rival_ou.x][rival_ou.y + 1]) &&
888           (@array[rival_ou.x][rival_ou.y + 1].name == "FU") &&
889           (@array[rival_ou.x][rival_ou.y + 1].sente == sente)) # uchifu true
890         fu_x = rival_ou.x
891         fu_y = rival_ou.y + 1
892       else
893         return false
894       end
895     else                        # gote
896       if ((rival_ou.y != 0) &&
897           (@array[rival_ou.x][rival_ou.y - 1]) &&
898           (@array[rival_ou.x][rival_ou.y - 1].name == "FU") &&
899           (@array[rival_ou.x][rival_ou.y - 1].sente == sente)) # uchifu true
900         fu_x = rival_ou.x
901         fu_y = rival_ou.y - 1
902       else
903         return false
904       end
905     end
906     
907     ## case: rival_ou is moving
908     escaped = false
909     rival_ou.movable_grids.each do |(cand_x, cand_y)|
910       tmp_board = Marshal.load(Marshal.dump(self))
911       s = tmp_board.move_to(rival_ou.x, rival_ou.y, cand_x, cand_y, "OU", ! sente)
912       raise "internal error" if (s != true)
913       if (! tmp_board.checkmated?(! sente)) # good move
914         return false
915       end
916     end
917
918     ## case: rival is capturing fu
919     x = 1
920     while (x <= 9)
921       y = 1
922       while (y <= 9)
923         if (@array[x][y] &&
924             (@array[x][y].sente != sente) &&
925             @array[x][y].movable_grids.include?([fu_x, fu_y])) # capturable
926           if (@array[x][y].promoted)
927             name = @array[x][y].promoted_name
928           else
929             name = @array[x][y].name
930           end
931           tmp_board = Marshal.load(Marshal.dump(self))
932           s = tmp_board.move_to(x, y, fu_x, fu_y, name, ! sente)
933           raise "internal error" if (s != true)
934           if (! tmp_board.checkmated?(! sente)) # good move
935             return false
936           end
937         end
938         y = y + 1
939       end
940       x = x + 1
941     end
942     return true
943   end
944
945   def oute_sennichite?(sente)
946     if (checkmated?(! sente))
947       str = to_s
948       if (sente)
949         if (@sente_history[str] && (@sente_history[str] >= 3)) # already 3 times
950           return true
951         end
952       else
953         if (@gote_history[str] && (@gote_history[str] >= 3)) # already 3 times
954           return true
955         end
956       end
957     end
958     return false
959   end
960
961   def sennichite?(sente)
962     str = to_s
963     if (@history[str] && (@history[str] >= 3)) # already 3 times
964       return true
965     end
966     return false
967   end
968
969   def handle_one_move(str)
970     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
971       sg = $1
972       x0 = $2.to_i
973       y0 = $3.to_i
974       x1 = $4.to_i
975       y1 = $5.to_i
976       name = $6
977     elsif (str =~ /^%KACHI/)
978       return :kachi
979     elsif (str =~ /^%TORYO/)
980       return :toryo
981     else
982       return :illegal
983     end
984     
985     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
986         ((x0 != 0) || (y0 != 0)))
987       return :illegal
988     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
989       return :illegal
990     end
991     
992     if (sg == "+")
993       sente = true
994       hands = @sente_hands
995     else
996       sente = false
997       hands = @gote_hands
998     end
999     
1000     ## source check
1001     if ((x0 == 0) && (y0 == 0))
1002       return :illegal if (! have_piece?(hands, name))
1003     elsif (! @array[x0][y0])
1004       return :illegal           # no piece
1005     elsif (@array[x0][y0].sente != sente)
1006       return :illegal           # this is not mine
1007     elsif (@array[x0][y0].name != name)
1008       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
1009     end
1010
1011     ## destination check
1012     if (@array[x1][y1] &&
1013         (@array[x1][y1].sente == sente)) # can't capture mine
1014       return :illegal
1015     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
1016       return :illegal           # can't put on existing piece
1017     end
1018
1019     tmp_board = Marshal.load(Marshal.dump(self))
1020     return :illegal if (tmp_board.move_to(x0, y0, x1, y1, name, sente) == :illegal)
1021     return :oute_kaihimore if (tmp_board.checkmated?(sente))
1022     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1023     return :sennichite if tmp_board.sennichite?(sente)
1024
1025     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1026       return :uchifuzume
1027     end
1028
1029     move_to(x0, y0, x1, y1, name, sente)
1030     str = to_s
1031
1032     if (checkmated?(! sente))
1033       if (sente)
1034         @sente_history[str] = (@sente_history[str] || 0) + 1
1035       else
1036         @gote_history[str] = (@gote_history[str] || 0) + 1
1037       end
1038     else
1039       if (sente)
1040         @sente_history.clear
1041       else
1042         @gote_history.clear
1043       end
1044     end
1045     @history[str] = (@history[str] || 0) + 1
1046     return :normal
1047   end
1048
1049   def to_s
1050     a = Array::new
1051     y = 1
1052     while (y <= 9)
1053       a.push(sprintf("P%d", y))
1054       x = 9
1055       while (x >= 1)
1056         piece = @array[x][y]
1057         if (piece)
1058           s = piece.to_s
1059         else
1060           s = " * "
1061         end
1062         a.push(s)
1063         x = x - 1
1064       end
1065       a.push(sprintf("\n"))
1066       y = y + 1
1067     end
1068     if (! sente_hands.empty?)
1069       a.push("P+")
1070       sente_hands.each do |p|
1071         a.push("00" + p.name)
1072       end
1073       a.push("\n")
1074     end
1075     if (! gote_hands.empty?)
1076       a.push("P-")
1077       gote_hands.each do |p|
1078         a.push("00" + p.name)
1079       end
1080       a.push("\n")
1081     end
1082     a.push("+\n")
1083     return a.join
1084   end
1085 end
1086
1087 class Game
1088   def initialize(game_name, player0, player1)
1089     @monitors = Array::new
1090     @game_name = game_name
1091     if (@game_name =~ /-(\d+)-(\d+)$/)
1092       @total_time = $1.to_i
1093       @byoyomi = $2.to_i
1094     end
1095
1096     if (player0.sente)
1097       @sente = player0
1098       @gote = player1
1099     else
1100       @sente = player1
1101       @gote = player0
1102     end
1103     @current_player = @sente
1104     @next_player = @gote
1105
1106     @sente.game = self
1107     @gote.game = self
1108
1109     @last_move = ""
1110     @current_turn = 0
1111
1112     @sente.status = "agree_waiting"
1113     @gote.status = "agree_waiting"
1114     @id = sprintf("%s+%s+%s+%s+%s", 
1115                   LEAGUE.event, @game_name, @sente.name, @gote.name,
1116                   Time::new.strftime("%Y%m%d%H%M%S"))
1117
1118     LEAGUE.games[@id] = self
1119
1120
1121     log_message(sprintf("game created %s", @id))
1122
1123     @logfile = @id + ".csa"
1124     @board = Board::new
1125     @board.initial
1126     @start_time = nil
1127     @fh = nil
1128
1129     propose
1130   end
1131   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1132   attr_accessor :last_move, :current_turn
1133
1134   def monitoron(monitor)
1135     @monitors.delete(monitor)
1136     @monitors.push(monitor)
1137   end
1138
1139   def monitoroff(monitor)
1140     @monitors.delete(monitor)
1141   end
1142
1143   def reject(rejector)
1144     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1145     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1146     finish
1147   end
1148
1149   def kill(killer)
1150     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1151       reject(killer.name)
1152     elsif (@current_player == killer)
1153       abnormal_lose()
1154       finish
1155     end
1156   end
1157
1158   def finish
1159     log_message(sprintf("game finished %s", @id))
1160     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1161     @fh.close
1162
1163     @sente.game = nil
1164     @gote.game = nil
1165     @sente.status = "connected"
1166     @gote.status = "connected"
1167
1168     if (@current_player.protocol == "CSA")
1169       @current_player.finish
1170     end
1171     if (@next_player.protocol == "CSA")
1172       @next_player.finish
1173     end
1174     @monitors = Array::new
1175     @sente = nil
1176     @gote = nil
1177     @current_player = nil
1178     @next_player = nil
1179     LEAGUE.games.delete(@id)
1180   end
1181
1182   def handle_one_move(str, player)
1183     finish_flag = true
1184     if (@current_player == player)
1185       @end_time = Time::new
1186       t = (@end_time - @start_time).ceil
1187       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1188       
1189       move_status = nil
1190       if ((@current_player.mytime - t <= 0) && (@total_time > 0))
1191         status = :timeout
1192       elsif (str == :timeout)
1193         return false            # time isn't expired. players aren't swapped. continue game
1194       else
1195 #        begin
1196           move_status = @board.handle_one_move(str)
1197 #        rescue
1198 #          log_error("handle_one_move raise exception for #{str}")
1199 #          move_status = :illegal
1200 #        end
1201         if ((move_status == :illegal) || (move_status == :uchifuzme) || (move_status == :oute_kaihimore))
1202           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1203         else
1204           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1205             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1206             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1207             @fh.printf("%s\nT%d\n", str, t)
1208             @last_move = sprintf("%s,T%d", str, t)
1209             @current_turn = @current_turn + 1
1210           end
1211           @monitors.each do |monitor|
1212             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1213             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1214           end
1215         end
1216       end
1217
1218       if (@current_player.mytime - t < @byoyomi)
1219         @current_player.mytime = @byoyomi
1220       else
1221         @current_player.mytime = @current_player.mytime - t
1222       end
1223
1224       if (@next_player.status != "game") # rival is logout or disconnected
1225         abnormal_win()
1226       elsif (status == :timeout)
1227         timeout_lose()
1228       elsif (move_status == :illegal)
1229         illegal_lose()
1230       elsif (move_status == :kachi)
1231         kachi_win()
1232       elsif (move_status == :toryo)
1233         toryo_lose()
1234       elsif (move_status == :outori)
1235         outori_win()
1236       elsif (move_status == :sennichite)
1237         sennichite_draw()
1238       elsif (move_status == :oute_sennichite)
1239         oute_sennichite_lose()
1240       elsif (move_status == :uchifuzume)
1241         uchifuzume_lose()
1242       elsif (move_status == :oute_kaihimore)
1243         oute_kaihimore_lose()
1244       else
1245         finish_flag = false
1246       end
1247       finish() if finish_flag
1248       (@current_player, @next_player) = [@next_player, @current_player]
1249       @start_time = Time::new
1250       return finish_flag
1251     end
1252   end
1253
1254   def abnormal_win
1255     @current_player.status = "connected"
1256     @next_player.status = "connected"
1257     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1258     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1259     @fh.printf("%%TORYO\n")
1260     @fh.print(@board.to_s.gsub(/^/, "\'"))
1261     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1262     @monitors.each do |monitor|
1263       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1264     end
1265   end
1266
1267   def abnormal_lose
1268     @current_player.status = "connected"
1269     @next_player.status = "connected"
1270     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1271     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1272     @fh.printf("%%TORYO\n")
1273     @fh.print(@board.to_s.gsub(/^/, "\'"))
1274     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1275     @monitors.each do |monitor|
1276       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1277     end
1278   end
1279
1280   def sennichite_draw
1281     @current_player.status = "connected"
1282     @next_player.status = "connected"
1283     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1284     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1285     @fh.print(@board.to_s.gsub(/^/, "\'"))
1286     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1287     @monitors.each do |monitor|
1288       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1289     end
1290   end
1291
1292   def oute_sennichite_lose
1293     @current_player.status = "connected"
1294     @next_player.status = "connected"
1295     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1296     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1297     @fh.print(@board.to_s.gsub(/^/, "\'"))
1298     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1299     @monitors.each do |monitor|
1300       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1301     end
1302   end
1303
1304   def illegal_lose
1305     @current_player.status = "connected"
1306     @next_player.status = "connected"
1307     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1308     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1309     @fh.print(@board.to_s.gsub(/^/, "\'"))
1310     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1311     @monitors.each do |monitor|
1312       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1313     end
1314   end
1315
1316   def uchifuzume_lose
1317     @current_player.status = "connected"
1318     @next_player.status = "connected"
1319     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1320     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1321     @fh.print(@board.to_s.gsub(/^/, "\'"))
1322     @fh.printf("'summary:uchifuzume:%s lose:%s win\n", @current_player.name, @next_player.name)
1323     @monitors.each do |monitor|
1324       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1325     end
1326   end
1327
1328   def oute_kaihimore_lose
1329     @current_player.status = "connected"
1330     @next_player.status = "connected"
1331     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1332     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1333     @fh.print(@board.to_s.gsub(/^/, "\'"))
1334     @fh.printf("'summary:oute_kaihimore:%s lose:%s win\n", @current_player.name, @next_player.name)
1335     @monitors.each do |monitor|
1336       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1337     end
1338   end
1339
1340   def timeout_lose
1341     @current_player.status = "connected"
1342     @next_player.status = "connected"
1343     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1344     @next_player.write_safe("#TIME_UP\n#WIN\n")
1345     @fh.print(@board.to_s.gsub(/^/, "\'"))
1346     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1347     @monitors.each do |monitor|
1348       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1349     end
1350   end
1351
1352   def kachi_win
1353     @current_player.status = "connected"
1354     @next_player.status = "connected"
1355     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1356     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1357     @fh.printf("%%KACHI\n")
1358     @fh.print(@board.to_s.gsub(/^/, "\'"))
1359     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1360     @monitors.each do |monitor|
1361       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1362     end
1363   end
1364
1365   def toryo_lose
1366     @current_player.status = "connected"
1367     @next_player.status = "connected"
1368     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1369     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1370     @fh.printf("%%TORYO\n")
1371     @fh.print(@board.to_s.gsub(/^/, "\'"))
1372     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
1373     @monitors.each do |monitor|
1374       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1375     end
1376   end
1377
1378   def outori_win
1379     @current_player.status = "connected"
1380     @next_player.status = "connected"
1381     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1382     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1383     @fh.print(@board.to_s.gsub(/^/, "\'"))
1384     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1385     @monitors.each do |monitor|
1386       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1387     end
1388   end
1389
1390   def start
1391     log_message(sprintf("game started %s", @id))
1392     @sente.write_safe(sprintf("START:%s\n", @id))
1393     @gote.write_safe(sprintf("START:%s\n", @id))
1394     @sente.mytime = @total_time
1395     @gote.mytime = @total_time
1396     @start_time = Time::new
1397   end
1398
1399   def propose
1400     begin
1401       @fh = open(@logfile, "w")
1402       @fh.sync = true
1403
1404       @fh.printf("V2\n")
1405       @fh.printf("N+%s\n", @sente.name)
1406       @fh.printf("N-%s\n", @gote.name)
1407       @fh.printf("$EVENT:%s\n", @id)
1408
1409       @sente.write_safe(propose_message("+"))
1410       @gote.write_safe(propose_message("-"))
1411
1412       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1413       @fh.print <<EOM
1414 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1415 P2 * -HI *  *  *  *  * -KA * 
1416 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1417 P4 *  *  *  *  *  *  *  *  * 
1418 P5 *  *  *  *  *  *  *  *  * 
1419 P6 *  *  *  *  *  *  *  *  * 
1420 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1421 P8 * +KA *  *  *  *  * +HI * 
1422 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1423 +
1424 EOM
1425     end
1426   end
1427
1428   def show()
1429     str0 = <<EOM
1430 BEGIN Game_Summary
1431 Protocol_Version:1.0
1432 Protocol_Mode:Server
1433 Format:Shogi 1.0
1434 Game_ID:#{@id}
1435 Name+:#{@sente.name}
1436 Name-:#{@gote.name}
1437 Rematch_On_Draw:NO
1438 To_Move:+
1439 BEGIN Time
1440 Time_Unit:1sec
1441 Total_Time:#{@total_time}
1442 Byoyomi:#{@byoyomi}
1443 Least_Time_Per_Move:#{Least_Time_Per_Move}
1444 Remaining_Time+:#{@sente.mytime}
1445 Remaining_Time-:#{@gote.mytime}
1446 Last_Move:#{@last_move}
1447 Current_Turn:#{@current_turn}
1448 END Time
1449 BEGIN Position
1450 Jishogi_Declaration:1.1
1451 EOM
1452
1453     str1 = <<EOM
1454 END Position
1455 END Game_Summary
1456 EOM
1457
1458     return str0 + @board.to_s + str1
1459   end
1460
1461   def propose_message(sg_flag)
1462     str = <<EOM
1463 BEGIN Game_Summary
1464 Protocol_Version:1.0
1465 Protocol_Mode:Server
1466 Format:Shogi 1.0
1467 Game_ID:#{@id}
1468 Name+:#{@sente.name}
1469 Name-:#{@gote.name}
1470 Your_Turn:#{sg_flag}
1471 Rematch_On_Draw:NO
1472 To_Move:+
1473 BEGIN Time
1474 Time_Unit:1sec
1475 Total_Time:#{@total_time}
1476 Byoyomi:#{@byoyomi}
1477 Least_Time_Per_Move:#{Least_Time_Per_Move}
1478 END Time
1479 BEGIN Position
1480 Jishogi_Declaration:1.1
1481 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1482 P2 * -HI *  *  *  *  * -KA * 
1483 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1484 P4 *  *  *  *  *  *  *  *  * 
1485 P5 *  *  *  *  *  *  *  *  * 
1486 P6 *  *  *  *  *  *  *  *  * 
1487 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1488 P8 * +KA *  *  *  *  * +HI * 
1489 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1490 P+
1491 P-
1492 +
1493 END Position
1494 END Game_Summary
1495 EOM
1496     return str
1497   end
1498 end
1499
1500 def usage
1501     print <<EOM
1502 NAME
1503         shogi-server - server for CSA server protocol
1504
1505 SYNOPSIS
1506         shogi-server event_name port_number
1507
1508 DESCRIPTION
1509         server for CSA server protocol
1510
1511 OPTIONS
1512         --pid-file file
1513                 specify filename for logging process ID
1514
1515 LICENSE
1516         this file is distributed under GPL version2 and might be compiled by Exerb
1517
1518 SEE ALSO
1519
1520 RELEASE
1521         #{Release}
1522
1523 REVISION
1524         #{Revision}
1525 EOM
1526 end
1527
1528 def log_message(str)
1529   printf("%s message: %s\n", Time::new.to_s, str)
1530 end
1531
1532 def log_warning(str)
1533   printf("%s warning: %s\n", Time::new.to_s, str)
1534 end
1535
1536 def log_error(str)
1537   printf("%s error: %s\n", Time::new.to_s, str)
1538 end
1539
1540
1541 def parse_command_line
1542   options = Hash::new
1543   parser = GetoptLong.new
1544   parser.ordering = GetoptLong::REQUIRE_ORDER
1545   parser.set_options(
1546                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1547
1548   parser.quiet = true
1549   begin
1550     parser.each_option do |name, arg|
1551       name.sub!(/^--/, '')
1552       options[name] = arg.dup
1553     end
1554   rescue
1555     usage
1556     raise parser.error_message
1557   end
1558   return options
1559 end
1560
1561 def good_game_name?(str)
1562   if ((str =~ /^(.+)-\d+-\d+$/) &&
1563       (good_identifier?($1)))
1564     return true
1565   else
1566     return false
1567   end
1568 end
1569
1570 def good_identifier?(str)
1571   if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
1572       (str.length < Max_Identifier_Length))
1573     return true
1574   else
1575     return false
1576   end
1577 end
1578
1579 def good_login?(str)
1580   tokens = str.split
1581   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1582       (tokens[0] == "LOGIN") &&
1583       (good_identifier?(tokens[1])))
1584     return true
1585   else
1586     return false
1587   end
1588 end
1589
1590 def  write_pid_file(file)
1591   open(file, "w") do |fh|
1592     fh.print Process::pid, "\n"
1593   end
1594 end
1595
1596 def mutex_watchdog(mutex, sec)
1597   while true
1598     begin
1599       timeout(sec) do
1600         mutex.lock
1601         mutex.unlock
1602       end
1603       sleep(sec)
1604     rescue TimeoutError
1605       log_error("mutex watchdog timeout")
1606       exit(1)
1607     end
1608   end
1609 end
1610
1611 def main
1612   $mutex = Mutex::new
1613   Thread::start do
1614     mutex_watchdog($mutex, 10)
1615   end
1616
1617   $options = parse_command_line
1618   if (ARGV.length != 2)
1619     usage
1620     exit 2
1621   end
1622
1623   LEAGUE.event = ARGV.shift
1624   port = ARGV.shift
1625
1626   write_pid_file($options["pid-file"]) if ($options["pid-file"])
1627
1628
1629   Thread.abort_on_exception = true
1630
1631   server = TCPserver.open(port)
1632   log_message("server started")
1633
1634   while true
1635     Thread::start(server.accept) do |client|
1636       client.sync = true
1637       player = nil
1638       while (str = client.gets_timeout(Login_Time))
1639         begin
1640           $mutex.lock
1641           str =~ /([\r\n]*)$/
1642           eol = $1
1643           if (good_login?(str))
1644             player = Player::new(str, client)
1645             if (LEAGUE.players[player.name])
1646               if ((LEAGUE.players[player.name].password == player.password) &&
1647                   (LEAGUE.players[player.name].status != "game"))
1648                 log_message(sprintf("user %s login forcely", player.name))
1649                 LEAGUE.players[player.name].kill
1650               else
1651                 client.write_safe("LOGIN:incorrect" + eol)
1652                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1653                 client.close
1654                 Thread::exit
1655               end
1656             end
1657             LEAGUE.add(player)
1658             break
1659           else
1660             client.write_safe("LOGIN:incorrect" + eol)
1661             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1662           end
1663         ensure
1664           $mutex.unlock
1665         end
1666       end                       # login loop
1667       if (! player)
1668         client.close
1669         Thread::exit
1670       end
1671       log_message(sprintf("user %s login", player.name))
1672       player.run
1673       begin
1674         $mutex.lock
1675         if (player.game)
1676           player.game.kill(player)
1677         end
1678         player.finish
1679         LEAGUE.delete(player)
1680         log_message(sprintf("user %s logout", player.name))
1681       ensure
1682         $mutex.unlock
1683       end
1684     end
1685   end
1686 end
1687
1688 if ($0 == __FILE__)
1689   LEAGUE = League::new
1690   main
1691 end