OSDN Git Service

finished status added
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$
3
4 ## Copyright (C) 2004 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 DEFAULT_TIMEOUT = 10            # for single socket operation
21 Total_Time = 1500
22 Least_Time_Per_Move = 1
23 Watchdog_Time = 30              # time for ping
24 Login_Time = 300                # time for LOGIN
25
26 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
27 Release.concat("-") if (Release == "")
28 Revision = "$Revision$".gsub(/[^\.\d]/, '')
29
30 STDOUT.sync = true
31 STDERR.sync = true
32
33 require 'getoptlong'
34 require 'thread'
35 require 'timeout'
36 require 'socket'
37 require 'ping'
38
39 TCPSocket.do_not_reverse_lookup = true
40
41 class TCPSocket
42   def gets_timeout(t = DEFAULT_TIMEOUT)
43     begin
44       timeout(t) do
45         return self.gets
46       end
47     rescue TimeoutError
48       return nil
49     rescue
50       return nil
51     end
52   end
53   def gets_safe(t = nil)
54     if (t && t > 0)
55       begin
56         timeout(t) do
57           return self.gets
58         end
59       rescue TimeoutError
60         return :timeout
61       rescue
62         return nil
63       end
64     else
65       begin
66         return self.gets
67       rescue
68         return nil
69       end
70     end
71   end
72   def write_safe(str)
73     begin
74       return self.write(str)
75     rescue
76       return nil
77     end
78   end
79 end
80
81
82 class League
83   def initialize
84     @games = Hash::new
85     @players = Hash::new
86   end
87   attr_accessor :players, :games
88
89   def add(player)
90     @players[player.name] = player
91   end
92   def delete(player)
93     @players.delete(player.name)
94   end
95   def duplicated?(player)
96     if (@players[player.name])
97       return true
98     else
99       return false
100     end
101   end
102   def get_player(status, game_name, sente, searcher=nil)
103     @players.each do |name, player|
104       if ((player.status == status) &&
105           (player.game_name == game_name) &&
106           ((player.sente == nil) || (player.sente == sente)) &&
107           ((searcher == nil) || (player != searcher)))
108         return player
109       end
110     end
111     return nil
112   end
113 end
114
115 class Player
116   def initialize(str, socket)
117     @name = nil
118     @password = nil
119     @socket = socket
120     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
121
122     @protocol = nil             # CSA or x1
123     @eol = "\m"                 # favorite eol code
124     @game = nil
125     @game_name = ""
126     @mytime = Total_Time        # set in start method also
127     @sente = nil
128     @watchdog_thread = nil
129
130     login(str)
131   end
132
133   attr_accessor :name, :password, :socket, :status
134   attr_accessor :protocol, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente
135
136   def finish
137     if (@status != "finished")
138       @status = "finished"
139       log_message(sprintf("user %s finish", @name))    
140       Thread::kill(@watchdog_thread) if @watchdog_thread
141       @socket.close if (! @socket.closed?)
142     end
143   end
144
145   def watchdog(time)
146     while true
147       begin
148         Ping.pingecho(@socket.addr[3])
149       rescue
150       end
151       sleep(time)
152     end
153   end
154
155   def to_s
156     if ((status == "game_waiting") ||
157         (status == "agree_waiting") ||
158         (status == "game"))
159       if (@sente)
160         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
161       elsif (@sente == false)
162         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
163       elsif (@sente == nil)
164         return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name)
165       end
166     else
167       return sprintf("%s %s %s", @name, @protocol, @status)
168     end
169   end
170
171   def write_help
172     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
173   end
174
175   def write_safe(str)
176     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
177   end
178
179   def login(str)
180     str =~ /([\r\n]*)$/
181     @eol = $1
182     str.chomp!
183     (login, @name, @password, ext) = str.split
184     if (ext)
185       @protocol = "x1"
186     else
187       @protocol = "CSA"
188     end
189     @watchdog_thread = Thread::start do
190       watchdog(Watchdog_Time)
191     end
192   end
193     
194   def run
195     write_safe(sprintf("LOGIN:%s OK\n", @name))
196     if (@protocol != "CSA")
197       log_message(sprintf("user %s run in %s mode", @name, @protocol))
198       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
199     else
200       log_message(sprintf("user %s run in CSA mode", @name))
201       csa_1st_str = "%%GAME default +-"
202     end
203     
204     while (csa_1st_str || (str = @socket.gets_safe(@mytime)))
205       begin
206         $mutex.lock
207         if (csa_1st_str)
208           str = csa_1st_str
209           csa_1st_str = nil
210         end
211         if (@status == "finished")
212           return
213         end
214         str.chomp! if (str.class == String)
215         case str
216         when /^[\+\-%][^%]/, :timeout
217           if (@status == "game")
218             s = @game.handle_one_move(str, self)
219             return if (s && @protocol == "CSA")
220           else
221             next
222           end
223         when /^AGREE/
224           if (@status == "agree_waiting")
225             @status = "start_waiting"
226             if ((@game.sente.status == "start_waiting") &&
227                 (@game.gote.status == "start_waiting"))
228               @game.start
229               @game.sente.status = "game"
230               @game.gote.status = "game"
231             end
232           else
233             write_safe("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)
234             next
235           end
236         when /^%%HELP/
237           write_help
238         when /^%%GAME\s+(\S+)\s+([\+\-]+)/
239           if ((@status == "connected") || (@status == "game_waiting"))
240             @status = "game_waiting"
241           else
242             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
243             next
244           end
245           @status = "game_waiting"
246           @game_name = $1
247           sente_str = $2
248           if (sente_str == "+")
249             @sente = true
250             rival_sente = false
251           elsif (sente_str == "-")
252             @sente = false
253             rival_sente = true
254           else
255             @sente = nil
256             rival_sente = nil
257           end
258           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
259           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
260           if (rival)
261             if (@sente == nil)
262               if (rand(2) == 0)
263                 @sente = true
264                 rival_sente = false
265               else
266                 @sente = false
267                 rival_sente = true
268               end
269             elsif (rival_sente == nil)
270               if (@sente)
271                 rival_sente = false
272               else
273                 rival_sente = true
274               end
275             end
276             rival.sente = rival_sente
277             Game::new(@game_name, self, rival)
278             self.status = "agree_waiting"
279             rival.status = "agree_waiting"
280           end
281         when /^%%CHAT\s+(.+)/
282           message = $1
283           LEAGUE.players.each do |name, player|
284             if (player.protocol != "CSA")
285               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
286               player.status = "zombie" if (! s)
287             end
288           end
289         when /^%%LIST/
290           buf = Array::new
291           LEAGUE.games.each do |id, game|
292             buf.push(sprintf("##[LIST] %s\n", id))
293           end
294           buf.push("##[LIST] +OK\n")
295           write_safe(buf.join)
296         when /^%%SHOW\s+(\S+)/
297           id = $1
298           if (LEAGUE.games[id])
299             write_safe(LEAGUE.games[id].board.to_s.gsub(/^/, '##[SHOW] '))
300           end
301           write_safe("##[SHOW] +OK\n")
302         when /^%%WHO/
303           buf = Array::new
304           LEAGUE.players.each do |name, player|
305             buf.push(sprintf("##[WHO] %s\n", player.to_s))
306           end
307           buf.push("##[WHO] +OK\n")
308           write_safe(buf.join)
309         when /^LOGOUT/
310           write_safe("LOGOUT:completed\n")
311           return
312         else
313           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
314         end
315       ensure
316         $mutex.unlock
317       end
318     end                         # enf of while
319   end
320 end
321
322 class Piece
323   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
324   def initialize(name, sente)
325     @name = name
326     @sente = sente
327     @promoted = false
328   end
329   attr_accessor :name, :promoted, :sente
330
331   def promoted_name
332     PROMOTE[name]
333   end
334
335   def to_s
336     if (@sente)
337       sg = "+"
338     else
339       sg = "-"
340     end
341     if (@promoted)
342       n = PROMOTE[@name]
343     else
344       n = @name
345     end
346     return sg + n
347   end
348 end
349
350
351
352 class Board
353   def initialize
354     @sente_hands = Array::new
355     @gote_hands = Array::new
356     @array = [[], [], [], [], [], [], [], [], [], []]
357   end
358   attr_accessor :array, :sente_hands, :gote_hands
359
360   def initial
361     @array[1][1] = Piece::new("KY", false)
362     @array[2][1] = Piece::new("KE", false)
363     @array[3][1] = Piece::new("GI", false)
364     @array[4][1] = Piece::new("KI", false)
365     @array[5][1] = Piece::new("OU", false)
366     @array[6][1] = Piece::new("KI", false)
367     @array[7][1] = Piece::new("GI", false)
368     @array[8][1] = Piece::new("KE", false)
369     @array[9][1] = Piece::new("KY", false)
370     @array[2][2] = Piece::new("KA", false)
371     @array[8][2] = Piece::new("HI", false)
372     @array[1][3] = Piece::new("FU", false)
373     @array[2][3] = Piece::new("FU", false)
374     @array[3][3] = Piece::new("FU", false)
375     @array[4][3] = Piece::new("FU", false)
376     @array[5][3] = Piece::new("FU", false)
377     @array[6][3] = Piece::new("FU", false)
378     @array[7][3] = Piece::new("FU", false)
379     @array[8][3] = Piece::new("FU", false)
380     @array[9][3] = Piece::new("FU", false)
381
382     @array[1][9] = Piece::new("KY", true)
383     @array[2][9] = Piece::new("KE", true)
384     @array[3][9] = Piece::new("GI", true)
385     @array[4][9] = Piece::new("KI", true)
386     @array[5][9] = Piece::new("OU", true)
387     @array[6][9] = Piece::new("KI", true)
388     @array[7][9] = Piece::new("GI", true)
389     @array[8][9] = Piece::new("KE", true)
390     @array[9][9] = Piece::new("KY", true)
391     @array[2][8] = Piece::new("HI", true)
392     @array[8][8] = Piece::new("KA", true)
393     @array[1][7] = Piece::new("FU", true)
394     @array[2][7] = Piece::new("FU", true)
395     @array[3][7] = Piece::new("FU", true)
396     @array[4][7] = Piece::new("FU", true)
397     @array[5][7] = Piece::new("FU", true)
398     @array[6][7] = Piece::new("FU", true)
399     @array[7][7] = Piece::new("FU", true)
400     @array[8][7] = Piece::new("FU", true)
401     @array[9][7] = Piece::new("FU", true)
402   end
403
404   def get_piece_from_hands(hands, name)
405     p = hands.find { |i|
406       i.name == name
407     }
408     if (p)
409       hands.delete(p)
410     end
411     return p
412   end
413
414   def handle_one_move(str)
415     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
416       p = $1
417       x0 = $2.to_i
418       y0 = $3.to_i
419       x1 = $4.to_i
420       y1 = $5.to_i
421       name = $6
422     elsif (str =~ /^%/)
423       return true
424     else
425       return false              # illegal move
426     end
427     if (p == "+")
428       sente = true
429       hands = @sente_hands
430     else
431       sente = false
432       hands = @gote_hands
433     end
434     if (@array[x1][y1])
435       if (@array[x1][y1] == sente) # this is mine
436         return false            
437       end
438       hands.push(@array[x1][y1])
439       @array[x1][y1] = nil
440     end
441     if ((x0 == 0) && (y0 == 0))
442       p = get_piece_from_hands(hands, name)
443       return false if (! p)     # i don't have this one
444       @array[x1][y1] = p
445       p.sente = sente
446       p.promoted = false
447     else
448       @array[x1][y1] = @array[x0][y0]
449       @array[x0][y0] = nil
450       if (@array[x1][y1].name != name) # promoted ?
451         return false if (@array[x1][y1].promoted_name != name) # can't promote
452         @array[x1][y1].promoted = true
453       end
454     end
455     return true                 # legal move
456   end
457
458   def to_s
459     a = Array::new
460     y = 1
461     while (y <= 9)
462       a.push(sprintf("P%d", y))
463       x = 9
464       while (x >= 1)
465         piece = @array[x][y]
466         if (piece)
467           s = piece.to_s
468         else
469           s = " * "
470         end
471         a.push(s)
472         x = x - 1
473       end
474       a.push(sprintf("\n"))
475       y = y + 1
476     end
477     if (! sente_hands.empty?)
478       a.push("P+")
479       sente_hands.each do |p|
480         a.push("00" + p.name)
481       end
482       a.push("\n")
483     end
484     if (! gote_hands.empty?)
485       a.push("P-")
486       gote_hands.each do |p|
487         a.push("00" + p.name)
488       end
489       a.push("\n")
490     end
491     return a.join
492   end
493 end
494
495 class Game
496   def initialize(game_name, player0, player1)
497     @game_name = game_name
498     if (player0.sente)
499       @sente = player0
500       @gote = player1
501     else
502       @sente = player1
503       @gote = player0
504     end
505     @current_player = @sente
506     @next_player = @gote
507
508     @sente.game = self
509     @gote.game = self
510
511     @sente.status = "agree_waiting"
512     @gote.status = "agree_waiting"
513     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
514     LEAGUE.games[@id] = self
515
516
517     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
518
519     @logfile = @id + ".csa"
520     @board = Board::new
521     @board.initial
522     @start_time = nil
523     @fh = nil
524
525     propose
526   end
527   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
528
529   def finish
530     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
531     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
532     @fh.close
533
534     @sente.game = nil
535     @gote.game = nil
536     @sente.status = "connected"
537     @gote.status = "connected"
538     if (@current_player.protocol == "CSA")
539       @current_player.finish
540     end
541     if (@next_player.protocol == "CSA")
542       @next_player.finish
543     end
544     LEAGUE.games.delete(@id)
545   end
546
547   def handle_one_move(str, player)
548     finish_flag = false
549     if (@current_player == player)
550       @end_time = Time::new
551       t = @end_time - @start_time
552       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
553       if (str != :timeout)
554         legal_move = @board.handle_one_move(str)
555         if (legal_move)
556           @sente.write_safe(sprintf("%s,T%d\n", str, t))
557           @gote.write_safe(sprintf("%s,T%d\n", str, t))
558           @fh.printf("%s\nT%d\n", str, t)
559         else
560           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
561         end
562         @current_player.mytime = @current_player.mytime - t
563       else
564         @current_player.mytime = 0
565       end
566       if (!legal_move)
567         illegal_end()
568         finish_flag = true
569       elsif (@current_player.mytime <= 0)
570         timeout_end()
571         finish_flag = true
572       elsif (str =~ /%KACHI/)
573         kachi_end()
574         finish_flag = true
575       elsif (str =~ /%TORYO/)
576         toryo_end()
577         finish_flag = true
578       end
579       (@current_player, @next_player) = [@next_player, @current_player]
580       @start_time = Time::new
581       return finish_flag
582     end
583   end
584
585   def illegal_end
586     @current_player.status = "connected"
587     @next_player.status = "connected"
588     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
589     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
590   end
591
592   def timeout_end
593     @current_player.status = "connected"
594     @next_player.status = "connected"
595     @current_player.write_safe("#TIME_UP\n#LOSE\n")
596     @next_player.write_safe("#TIME_UP\n#WIN\n")
597   end
598
599   def kachi_end
600     @current_player.status = "connected"
601     @next_player.status = "connected"
602     @current_player.write_safe("#JISHOGI\n#WIN\n")
603     @next_player.write_safe("#JISHOGI\n#LOSE\n")
604   end
605
606   def toryo_end
607     @current_player.status = "connected"
608     @next_player.status = "connected"
609     @current_player.write_safe("#RESIGN\n#LOSE\n")
610     @next_player.write_safe("#RESIGN\n#WIN\n")
611   end
612
613   def start
614     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
615     @sente.write_safe(sprintf("START:%s\n", @id))
616     @gote.write_safe(sprintf("START:%s\n", @id))
617     @mytime = Total_Time    
618     @start_time = Time::new
619   end
620
621   def propose
622     begin
623       @fh = open(@logfile, "w")
624       @fh.sync = true
625
626       @fh.printf("V2\n")
627       @fh.printf("N+%s\n", @sente.name)
628       @fh.printf("N-%s\n", @gote.name)
629       @fh.printf("$EVENT:%s\n", @id)
630
631       @sente.write_safe(propose_message("+"))
632       @gote.write_safe(propose_message("-"))
633
634       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
635       @fh.print <<EOM
636 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
637 P2 * -HI *  *  *  *  * -KA *
638 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
639 P4 *  *  *  *  *  *  *  *  *
640 P5 *  *  *  *  *  *  *  *  *
641 P6 *  *  *  *  *  *  *  *  *
642 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
643 P8 * +KA *  *  *  *  * +HI *
644 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
645 +
646 EOM
647     end
648   end
649
650   def propose_message(sg_flag)
651     str = <<EOM
652 BEGIN Game_Summary
653 Protocol_Version:1.0
654 Protocol_Mode:Server
655 Format:Shogi 1.0
656 Game_ID:#{@id}
657 Name+:#{@sente.name}
658 Name-:#{@gote.name}
659 Your_Turn:#{sg_flag}
660 Rematch_On_Draw:NO
661 To_Move:+
662 BEGIN Time
663 Time_Unit:1sec
664 Total_Time:#{Total_Time}
665 Least_Time_Per_Move:#{Least_Time_Per_Move}
666 END Time
667 BEGIN Position
668 Jishogi_Declaration:1.1
669 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
670 P2 * -HI *  *  *  *  * -KA *
671 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
672 P4 *  *  *  *  *  *  *  *  *
673 P5 *  *  *  *  *  *  *  *  *
674 P6 *  *  *  *  *  *  *  *  *
675 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
676 P8 * +KA *  *  *  *  * +HI *
677 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
678 P+
679 P-
680 +
681 END Position
682 END Game_Summary
683 EOM
684     return str
685   end
686 end
687
688 def usage
689     print <<EOM
690 NAME
691         shogi-server - server for CSA server protocol
692
693 SYNOPSIS
694         shogi-server event_name port_number
695
696 DESCRIPTION
697         server for CSA server protocol
698
699 OPTIONS
700         --pid-file file
701                 specify filename for logging process ID
702
703 LICENSE
704         this file is distributed under GPL version2 and might be compiled by Exerb
705
706 SEE ALSO
707
708 RELEASE
709         #{Release}
710
711 REVISION
712         #{Revision}
713 EOM
714 end
715
716 def log_message(str)
717   printf("%s message: %s\n", Time::new.to_s, str)
718 end
719
720 def log_warning(str)
721   printf("%s message: %s\n", Time::new.to_s, str)
722 end
723
724 def log_error(str)
725   printf("%s error: %s\n", Time::new.to_s, str)
726 end
727
728
729 def parse_command_line
730   options = Hash::new
731   parser = GetoptLong.new
732   parser.ordering = GetoptLong::REQUIRE_ORDER
733   parser.set_options(
734                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
735
736   parser.quiet = true
737   begin
738     parser.each_option do |name, arg|
739       name.sub!(/^--/, '')
740       options[name] = arg.dup
741     end
742   rescue
743     usage
744     raise parser.error_message
745   end
746   return options
747 end
748
749 LEAGUE = League::new
750
751 def good_login?(str)
752   return false if (str !~ /^LOGIN /)
753   tokens = str.split
754   if ((tokens.length == 3) || 
755       ((tokens.length == 4) && tokens[3] == "x1"))
756     ## ok
757   else
758     return false
759   end
760   return true
761 end
762
763 def  write_pid_file(file)
764   open(file, "w") do |fh|
765     fh.print Process::pid, "\n"
766   end
767 end
768
769 def main
770   $mutex = Mutex::new
771   $options = parse_command_line
772   if (ARGV.length != 2)
773     usage
774     exit 2
775   end
776   event = ARGV.shift
777   port = ARGV.shift
778
779   write_pid_file($options["pid-file"]) if ($options["pid-file"])
780
781
782   Thread.abort_on_exception = true
783
784   server = TCPserver.open(port)
785   log_message("server started")
786
787   while true
788     Thread::start(server.accept) do |client|
789       client.sync = true
790       player = nil
791       while (str = client.gets_timeout(Login_Time))
792         begin
793           $mutex.lock
794           Thread::kill(Thread::current) if (! str) # disconnected
795           str =~ /([\r\n]*)$/
796           eol = $1
797           if (good_login?(str))
798             player = Player::new(str, client)
799             if (LEAGUE.duplicated?(player))
800               client.write_safe("LOGIN:incorrect" + eol)
801               client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
802               client.close
803               Thread::kill(Thread::current)
804             end
805             LEAGUE.add(player)
806             break
807           else
808             client.write_safe("LOGIN:incorrect" + eol)
809             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
810             client.close
811             Thread::kill(Thread::current)
812           end
813         ensure
814           $mutex.unlock
815         end
816       end                       # login loop
817       log_message(sprintf("user %s login", player.name))
818       player.run
819       begin
820         $mutex.lock
821         if (player.game)
822           player.game.finish
823         elsif (player.status != "finished")
824           player.finish
825         end
826         LEAGUE.delete(player)
827         log_message(sprintf("user %s logout", player.name))
828       ensure
829         $mutex.unlock
830       end
831     end
832   end
833 end
834
835 if ($0 == __FILE__)
836   main
837 end