OSDN Git Service

oute_sennichite fixed
[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     if (tmp_board.checkmated?(sente))
1022       return :illegal
1023     end
1024     return :oute_sennichite if tmp_board.oute_sennichite?(sente)
1025     return :sennichite if tmp_board.sennichite?(sente)
1026
1027     if ((x0 == 0) && (y0 == 0) && (name == "FU") && tmp_board.uchifuzume?(sente))
1028       return :illegal
1029     end
1030
1031     move_to(x0, y0, x1, y1, name, sente)
1032     str = to_s
1033
1034     if (checkmated?(! sente))
1035       if (sente)
1036         @sente_history[str] = (@sente_history[str] || 0) + 1
1037       else
1038         @gote_history[str] = (@gote_history[str] || 0) + 1
1039       end
1040     else
1041       if (sente)
1042         @sente_history.clear
1043       else
1044         @gote_history.clear
1045       end
1046     end
1047     @history[str] = (@history[str] || 0) + 1
1048     return :normal
1049   end
1050
1051   def to_s
1052     a = Array::new
1053     y = 1
1054     while (y <= 9)
1055       a.push(sprintf("P%d", y))
1056       x = 9
1057       while (x >= 1)
1058         piece = @array[x][y]
1059         if (piece)
1060           s = piece.to_s
1061         else
1062           s = " * "
1063         end
1064         a.push(s)
1065         x = x - 1
1066       end
1067       a.push(sprintf("\n"))
1068       y = y + 1
1069     end
1070     if (! sente_hands.empty?)
1071       a.push("P+")
1072       sente_hands.each do |p|
1073         a.push("00" + p.name)
1074       end
1075       a.push("\n")
1076     end
1077     if (! gote_hands.empty?)
1078       a.push("P-")
1079       gote_hands.each do |p|
1080         a.push("00" + p.name)
1081       end
1082       a.push("\n")
1083     end
1084     a.push("+\n")
1085     return a.join
1086   end
1087 end
1088
1089 class Game
1090   def initialize(game_name, player0, player1)
1091     @monitors = Array::new
1092     @game_name = game_name
1093     if (@game_name =~ /-(\d+)-(\d+)$/)
1094       @total_time = $1.to_i
1095       @byoyomi = $2.to_i
1096     end
1097
1098     if (player0.sente)
1099       @sente = player0
1100       @gote = player1
1101     else
1102       @sente = player1
1103       @gote = player0
1104     end
1105     @current_player = @sente
1106     @next_player = @gote
1107
1108     @sente.game = self
1109     @gote.game = self
1110
1111     @last_move = ""
1112     @current_turn = 0
1113
1114     @sente.status = "agree_waiting"
1115     @gote.status = "agree_waiting"
1116     @id = sprintf("%s+%s+%s+%s+%s", 
1117                   LEAGUE.event, @game_name, @sente.name, @gote.name,
1118                   Time::new.strftime("%Y%m%d%H%M%S"))
1119
1120     LEAGUE.games[@id] = self
1121
1122
1123     log_message(sprintf("game created %s", @id))
1124
1125     @logfile = @id + ".csa"
1126     @board = Board::new
1127     @board.initial
1128     @start_time = nil
1129     @fh = nil
1130
1131     propose
1132   end
1133   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
1134   attr_accessor :last_move, :current_turn
1135
1136   def monitoron(monitor)
1137     @monitors.delete(monitor)
1138     @monitors.push(monitor)
1139   end
1140
1141   def monitoroff(monitor)
1142     @monitors.delete(monitor)
1143   end
1144
1145   def reject(rejector)
1146     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1147     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
1148     finish
1149   end
1150
1151   def kill(killer)
1152     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
1153       reject(killer.name)
1154     elsif (@current_player == killer)
1155       abnormal_lose()
1156       finish
1157     end
1158   end
1159
1160   def finish
1161     log_message(sprintf("game finished %s", @id))
1162     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
1163     @fh.close
1164
1165     @sente.game = nil
1166     @gote.game = nil
1167     @sente.status = "connected"
1168     @gote.status = "connected"
1169
1170     if (@current_player.protocol == "CSA")
1171       @current_player.finish
1172     end
1173     if (@next_player.protocol == "CSA")
1174       @next_player.finish
1175     end
1176     @monitors = Array::new
1177     @sente = nil
1178     @gote = nil
1179     @current_player = nil
1180     @next_player = nil
1181     LEAGUE.games.delete(@id)
1182   end
1183
1184   def handle_one_move(str, player)
1185     finish_flag = true
1186     if (@current_player == player)
1187       @end_time = Time::new
1188       t = (@end_time - @start_time).ceil
1189       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
1190       
1191       move_status = nil
1192       if ((@current_player.mytime - t <= 0) && (@total_time > 0))
1193         status = :timeout
1194       elsif (str == :timeout)
1195         return false            # time isn't expired. players aren't swapped. continue game
1196       else
1197 #        begin
1198           move_status = @board.handle_one_move(str)
1199 #        rescue
1200 #          log_error("handle_one_move raise exception for #{str}")
1201 #          move_status = :illegal
1202 #        end
1203         if (move_status == :illegal)
1204           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
1205         else
1206           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite) || (move_status == :oute_sennichite))
1207             @sente.write_safe(sprintf("%s,T%d\n", str, t))
1208             @gote.write_safe(sprintf("%s,T%d\n", str, t))
1209             @fh.printf("%s\nT%d\n", str, t)
1210             @last_move = sprintf("%s,T%d", str, t)
1211             @current_turn = @current_turn + 1
1212           end
1213           @monitors.each do |monitor|
1214             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
1215             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
1216           end
1217         end
1218       end
1219
1220       if (@current_player.mytime - t < @byoyomi)
1221         @current_player.mytime = @byoyomi
1222       else
1223         @current_player.mytime = @current_player.mytime - t
1224       end
1225
1226       if (@next_player.status != "game") # rival is logout or disconnected
1227         abnormal_win()
1228       elsif (status == :timeout)
1229         timeout_lose()
1230       elsif (move_status == :illegal)
1231         illegal_lose()
1232       elsif (move_status == :kachi)
1233         kachi_win()
1234       elsif (move_status == :toryo)
1235         toryo_lose()
1236       elsif (move_status == :outori)
1237         outori_win()
1238       elsif (move_status == :sennichite)
1239         sennichite_draw()
1240       elsif (move_status == :oute_sennichite)
1241         oute_sennichite_lose()
1242       else
1243         finish_flag = false
1244       end
1245       finish() if finish_flag
1246       (@current_player, @next_player) = [@next_player, @current_player]
1247       @start_time = Time::new
1248       return finish_flag
1249     end
1250   end
1251
1252   def abnormal_win
1253     @current_player.status = "connected"
1254     @next_player.status = "connected"
1255     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1256     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1257     @fh.printf("%%TORYO\n")
1258     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
1259     @monitors.each do |monitor|
1260       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1261     end
1262   end
1263
1264   def abnormal_lose
1265     @current_player.status = "connected"
1266     @next_player.status = "connected"
1267     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1268     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1269     @fh.printf("%%TORYO\n")
1270     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
1271     @monitors.each do |monitor|
1272       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
1273     end
1274   end
1275
1276   def sennichite_draw
1277     @current_player.status = "connected"
1278     @next_player.status = "connected"
1279     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
1280     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
1281     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
1282     @monitors.each do |monitor|
1283       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
1284     end
1285   end
1286
1287   def oute_sennichite_lose
1288     @current_player.status = "connected"
1289     @next_player.status = "connected"
1290     @current_player.write_safe("#OUTE_SENNICHITE\n#LOSE\n")
1291     @next_player.write_safe("#OUTE_SENNICHITE\n#WIN\n")
1292     @fh.printf("'summary:oute_sennichite:%s lose:%s win\n", @current_player.name, @next_player.name)
1293     @monitors.each do |monitor|
1294       monitor.write_safe(sprintf("##[MONITOR][%s] #OUTE_SENNICHITE\n", @id))
1295     end
1296   end
1297
1298   def illegal_lose
1299     @current_player.status = "connected"
1300     @next_player.status = "connected"
1301     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1302     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1303     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
1304     @monitors.each do |monitor|
1305       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1306     end
1307   end
1308
1309   def timeout_lose
1310     @current_player.status = "connected"
1311     @next_player.status = "connected"
1312     @current_player.write_safe("#TIME_UP\n#LOSE\n")
1313     @next_player.write_safe("#TIME_UP\n#WIN\n")
1314     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
1315     @monitors.each do |monitor|
1316       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
1317     end
1318   end
1319
1320   def kachi_win
1321     @current_player.status = "connected"
1322     @next_player.status = "connected"
1323     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
1324     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
1325     @fh.printf("%%KACHI\n")
1326     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
1327     @monitors.each do |monitor|
1328       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
1329     end
1330   end
1331
1332   def toryo_lose
1333     @current_player.status = "connected"
1334     @next_player.status = "connected"
1335     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
1336     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
1337     @fh.printf("%%TORYO\n")
1338     @fh.printf("'summary:toryo:%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 outori_win
1345     @current_player.status = "connected"
1346     @next_player.status = "connected"
1347     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
1348     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
1349     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
1350     @monitors.each do |monitor|
1351       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
1352     end
1353   end
1354
1355   def start
1356     log_message(sprintf("game started %s", @id))
1357     @sente.write_safe(sprintf("START:%s\n", @id))
1358     @gote.write_safe(sprintf("START:%s\n", @id))
1359     @sente.mytime = @total_time
1360     @gote.mytime = @total_time
1361     @start_time = Time::new
1362   end
1363
1364   def propose
1365     begin
1366       @fh = open(@logfile, "w")
1367       @fh.sync = true
1368
1369       @fh.printf("V2\n")
1370       @fh.printf("N+%s\n", @sente.name)
1371       @fh.printf("N-%s\n", @gote.name)
1372       @fh.printf("$EVENT:%s\n", @id)
1373
1374       @sente.write_safe(propose_message("+"))
1375       @gote.write_safe(propose_message("-"))
1376
1377       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
1378       @fh.print <<EOM
1379 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1380 P2 * -HI *  *  *  *  * -KA * 
1381 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1382 P4 *  *  *  *  *  *  *  *  * 
1383 P5 *  *  *  *  *  *  *  *  * 
1384 P6 *  *  *  *  *  *  *  *  * 
1385 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1386 P8 * +KA *  *  *  *  * +HI * 
1387 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1388 +
1389 EOM
1390     end
1391   end
1392
1393   def show()
1394     str0 = <<EOM
1395 BEGIN Game_Summary
1396 Protocol_Version:1.0
1397 Protocol_Mode:Server
1398 Format:Shogi 1.0
1399 Game_ID:#{@id}
1400 Name+:#{@sente.name}
1401 Name-:#{@gote.name}
1402 Rematch_On_Draw:NO
1403 To_Move:+
1404 BEGIN Time
1405 Time_Unit:1sec
1406 Total_Time:#{@total_time}
1407 Byoyomi:#{@byoyomi}
1408 Least_Time_Per_Move:#{Least_Time_Per_Move}
1409 Remaining_Time+:#{@sente.mytime}
1410 Remaining_Time-:#{@gote.mytime}
1411 Last_Move:#{@last_move}
1412 Current_Turn:#{@current_turn}
1413 END Time
1414 BEGIN Position
1415 Jishogi_Declaration:1.1
1416 EOM
1417
1418     str1 = <<EOM
1419 END Position
1420 END Game_Summary
1421 EOM
1422
1423     return str0 + @board.to_s + str1
1424   end
1425
1426   def propose_message(sg_flag)
1427     str = <<EOM
1428 BEGIN Game_Summary
1429 Protocol_Version:1.0
1430 Protocol_Mode:Server
1431 Format:Shogi 1.0
1432 Game_ID:#{@id}
1433 Name+:#{@sente.name}
1434 Name-:#{@gote.name}
1435 Your_Turn:#{sg_flag}
1436 Rematch_On_Draw:NO
1437 To_Move:+
1438 BEGIN Time
1439 Time_Unit:1sec
1440 Total_Time:#{@total_time}
1441 Byoyomi:#{@byoyomi}
1442 Least_Time_Per_Move:#{Least_Time_Per_Move}
1443 END Time
1444 BEGIN Position
1445 Jishogi_Declaration:1.1
1446 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
1447 P2 * -HI *  *  *  *  * -KA * 
1448 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
1449 P4 *  *  *  *  *  *  *  *  * 
1450 P5 *  *  *  *  *  *  *  *  * 
1451 P6 *  *  *  *  *  *  *  *  * 
1452 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
1453 P8 * +KA *  *  *  *  * +HI * 
1454 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
1455 P+
1456 P-
1457 +
1458 END Position
1459 END Game_Summary
1460 EOM
1461     return str
1462   end
1463 end
1464
1465 def usage
1466     print <<EOM
1467 NAME
1468         shogi-server - server for CSA server protocol
1469
1470 SYNOPSIS
1471         shogi-server event_name port_number
1472
1473 DESCRIPTION
1474         server for CSA server protocol
1475
1476 OPTIONS
1477         --pid-file file
1478                 specify filename for logging process ID
1479
1480 LICENSE
1481         this file is distributed under GPL version2 and might be compiled by Exerb
1482
1483 SEE ALSO
1484
1485 RELEASE
1486         #{Release}
1487
1488 REVISION
1489         #{Revision}
1490 EOM
1491 end
1492
1493 def log_message(str)
1494   printf("%s message: %s\n", Time::new.to_s, str)
1495 end
1496
1497 def log_warning(str)
1498   printf("%s warning: %s\n", Time::new.to_s, str)
1499 end
1500
1501 def log_error(str)
1502   printf("%s error: %s\n", Time::new.to_s, str)
1503 end
1504
1505
1506 def parse_command_line
1507   options = Hash::new
1508   parser = GetoptLong.new
1509   parser.ordering = GetoptLong::REQUIRE_ORDER
1510   parser.set_options(
1511                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
1512
1513   parser.quiet = true
1514   begin
1515     parser.each_option do |name, arg|
1516       name.sub!(/^--/, '')
1517       options[name] = arg.dup
1518     end
1519   rescue
1520     usage
1521     raise parser.error_message
1522   end
1523   return options
1524 end
1525
1526 def good_game_name?(str)
1527   if ((str =~ /^(.+)-\d+-\d+$/) &&
1528       (good_identifier?($1)))
1529     return true
1530   else
1531     return false
1532   end
1533 end
1534
1535 def good_identifier?(str)
1536   if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
1537       (str.length < Max_Identifier_Length))
1538     return true
1539   else
1540     return false
1541   end
1542 end
1543
1544 def good_login?(str)
1545   tokens = str.split
1546   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1547       (tokens[0] == "LOGIN") &&
1548       (good_identifier?(tokens[1])))
1549     return true
1550   else
1551     return false
1552   end
1553 end
1554
1555 def  write_pid_file(file)
1556   open(file, "w") do |fh|
1557     fh.print Process::pid, "\n"
1558   end
1559 end
1560
1561 def mutex_watchdog(mutex, sec)
1562   while true
1563     begin
1564       timeout(sec) do
1565         mutex.lock
1566         mutex.unlock
1567       end
1568       sleep(sec)
1569     rescue TimeoutError
1570       log_error("mutex watchdog timeout")
1571       exit(1)
1572     end
1573   end
1574 end
1575
1576 def main
1577   $mutex = Mutex::new
1578   Thread::start do
1579     mutex_watchdog($mutex, 10)
1580   end
1581
1582   $options = parse_command_line
1583   if (ARGV.length != 2)
1584     usage
1585     exit 2
1586   end
1587
1588   LEAGUE.event = ARGV.shift
1589   port = ARGV.shift
1590
1591   write_pid_file($options["pid-file"]) if ($options["pid-file"])
1592
1593
1594   Thread.abort_on_exception = true
1595
1596   server = TCPserver.open(port)
1597   log_message("server started")
1598
1599   while true
1600     Thread::start(server.accept) do |client|
1601       client.sync = true
1602       player = nil
1603       while (str = client.gets_timeout(Login_Time))
1604         begin
1605           $mutex.lock
1606           str =~ /([\r\n]*)$/
1607           eol = $1
1608           if (good_login?(str))
1609             player = Player::new(str, client)
1610             if (LEAGUE.players[player.name])
1611               if ((LEAGUE.players[player.name].password == player.password) &&
1612                   (LEAGUE.players[player.name].status != "game"))
1613                 log_message(sprintf("user %s login forcely", player.name))
1614                 LEAGUE.players[player.name].kill
1615               else
1616                 client.write_safe("LOGIN:incorrect" + eol)
1617                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1618                 client.close
1619                 Thread::exit
1620               end
1621             end
1622             LEAGUE.add(player)
1623             break
1624           else
1625             client.write_safe("LOGIN:incorrect" + eol)
1626             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1627           end
1628         ensure
1629           $mutex.unlock
1630         end
1631       end                       # login loop
1632       if (! player)
1633         client.close
1634         Thread::exit
1635       end
1636       log_message(sprintf("user %s login", player.name))
1637       player.run
1638       begin
1639         $mutex.lock
1640         if (player.game)
1641           player.game.kill(player)
1642         end
1643         player.finish
1644         LEAGUE.delete(player)
1645         log_message(sprintf("user %s logout", player.name))
1646       ensure
1647         $mutex.unlock
1648       end
1649     end
1650   end
1651 end
1652
1653 if ($0 == __FILE__)
1654   LEAGUE = League::new
1655   main
1656 end