OSDN Git Service

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