OSDN Git Service

set regal_move in case of timeout
[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       legal_move = true
554       if (str != :timeout)
555         legal_move = @board.handle_one_move(str)
556         if (legal_move)
557           @sente.write_safe(sprintf("%s,T%d\n", str, t))
558           @gote.write_safe(sprintf("%s,T%d\n", str, t))
559           @fh.printf("%s\nT%d\n", str, t)
560         else
561           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
562         end
563         @current_player.mytime = @current_player.mytime - t
564       else
565         @current_player.mytime = 0
566       end
567       if (!legal_move)
568         illegal_end()
569         finish_flag = true
570       elsif (@current_player.mytime <= 0)
571         timeout_end()
572         finish_flag = true
573       elsif (str =~ /%KACHI/)
574         kachi_end()
575         finish_flag = true
576       elsif (str =~ /%TORYO/)
577         toryo_end()
578         finish_flag = true
579       end
580       (@current_player, @next_player) = [@next_player, @current_player]
581       @start_time = Time::new
582       return finish_flag
583     end
584   end
585
586   def illegal_end
587     @current_player.status = "connected"
588     @next_player.status = "connected"
589     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
590     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
591   end
592
593   def timeout_end
594     @current_player.status = "connected"
595     @next_player.status = "connected"
596     @current_player.write_safe("#TIME_UP\n#LOSE\n")
597     @next_player.write_safe("#TIME_UP\n#WIN\n")
598   end
599
600   def kachi_end
601     @current_player.status = "connected"
602     @next_player.status = "connected"
603     @current_player.write_safe("#JISHOGI\n#WIN\n")
604     @next_player.write_safe("#JISHOGI\n#LOSE\n")
605   end
606
607   def toryo_end
608     @current_player.status = "connected"
609     @next_player.status = "connected"
610     @current_player.write_safe("#RESIGN\n#LOSE\n")
611     @next_player.write_safe("#RESIGN\n#WIN\n")
612   end
613
614   def start
615     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
616     @sente.write_safe(sprintf("START:%s\n", @id))
617     @gote.write_safe(sprintf("START:%s\n", @id))
618     @mytime = Total_Time    
619     @start_time = Time::new
620   end
621
622   def propose
623     begin
624       @fh = open(@logfile, "w")
625       @fh.sync = true
626
627       @fh.printf("V2\n")
628       @fh.printf("N+%s\n", @sente.name)
629       @fh.printf("N-%s\n", @gote.name)
630       @fh.printf("$EVENT:%s\n", @id)
631
632       @sente.write_safe(propose_message("+"))
633       @gote.write_safe(propose_message("-"))
634
635       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
636       @fh.print <<EOM
637 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
638 P2 * -HI *  *  *  *  * -KA *
639 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
640 P4 *  *  *  *  *  *  *  *  *
641 P5 *  *  *  *  *  *  *  *  *
642 P6 *  *  *  *  *  *  *  *  *
643 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
644 P8 * +KA *  *  *  *  * +HI *
645 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
646 +
647 EOM
648     end
649   end
650
651   def propose_message(sg_flag)
652     str = <<EOM
653 BEGIN Game_Summary
654 Protocol_Version:1.0
655 Protocol_Mode:Server
656 Format:Shogi 1.0
657 Game_ID:#{@id}
658 Name+:#{@sente.name}
659 Name-:#{@gote.name}
660 Your_Turn:#{sg_flag}
661 Rematch_On_Draw:NO
662 To_Move:+
663 BEGIN Time
664 Time_Unit:1sec
665 Total_Time:#{Total_Time}
666 Least_Time_Per_Move:#{Least_Time_Per_Move}
667 END Time
668 BEGIN Position
669 Jishogi_Declaration:1.1
670 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
671 P2 * -HI *  *  *  *  * -KA *
672 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
673 P4 *  *  *  *  *  *  *  *  *
674 P5 *  *  *  *  *  *  *  *  *
675 P6 *  *  *  *  *  *  *  *  *
676 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
677 P8 * +KA *  *  *  *  * +HI *
678 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
679 P+
680 P-
681 +
682 END Position
683 END Game_Summary
684 EOM
685     return str
686   end
687 end
688
689 def usage
690     print <<EOM
691 NAME
692         shogi-server - server for CSA server protocol
693
694 SYNOPSIS
695         shogi-server event_name port_number
696
697 DESCRIPTION
698         server for CSA server protocol
699
700 OPTIONS
701         --pid-file file
702                 specify filename for logging process ID
703
704 LICENSE
705         this file is distributed under GPL version2 and might be compiled by Exerb
706
707 SEE ALSO
708
709 RELEASE
710         #{Release}
711
712 REVISION
713         #{Revision}
714 EOM
715 end
716
717 def log_message(str)
718   printf("%s message: %s\n", Time::new.to_s, str)
719 end
720
721 def log_warning(str)
722   printf("%s message: %s\n", Time::new.to_s, str)
723 end
724
725 def log_error(str)
726   printf("%s error: %s\n", Time::new.to_s, str)
727 end
728
729
730 def parse_command_line
731   options = Hash::new
732   parser = GetoptLong.new
733   parser.ordering = GetoptLong::REQUIRE_ORDER
734   parser.set_options(
735                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
736
737   parser.quiet = true
738   begin
739     parser.each_option do |name, arg|
740       name.sub!(/^--/, '')
741       options[name] = arg.dup
742     end
743   rescue
744     usage
745     raise parser.error_message
746   end
747   return options
748 end
749
750 LEAGUE = League::new
751
752 def good_login?(str)
753   return false if (str !~ /^LOGIN /)
754   tokens = str.split
755   if ((tokens.length == 3) || 
756       ((tokens.length == 4) && tokens[3] == "x1"))
757     ## ok
758   else
759     return false
760   end
761   return true
762 end
763
764 def  write_pid_file(file)
765   open(file, "w") do |fh|
766     fh.print Process::pid, "\n"
767   end
768 end
769
770 def main
771   $mutex = Mutex::new
772   $options = parse_command_line
773   if (ARGV.length != 2)
774     usage
775     exit 2
776   end
777   event = ARGV.shift
778   port = ARGV.shift
779
780   write_pid_file($options["pid-file"]) if ($options["pid-file"])
781
782
783   Thread.abort_on_exception = true
784
785   server = TCPserver.open(port)
786   log_message("server started")
787
788   while true
789     Thread::start(server.accept) do |client|
790       client.sync = true
791       player = nil
792       while (str = client.gets_timeout(Login_Time))
793         begin
794           $mutex.lock
795           Thread::kill(Thread::current) if (! str) # disconnected
796           str =~ /([\r\n]*)$/
797           eol = $1
798           if (good_login?(str))
799             player = Player::new(str, client)
800             if (LEAGUE.duplicated?(player))
801               client.write_safe("LOGIN:incorrect" + eol)
802               client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
803               client.close
804               Thread::kill(Thread::current)
805             end
806             LEAGUE.add(player)
807             break
808           else
809             client.write_safe("LOGIN:incorrect" + eol)
810             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
811             client.close
812             Thread::kill(Thread::current)
813           end
814         ensure
815           $mutex.unlock
816         end
817       end                       # login loop
818       log_message(sprintf("user %s login", player.name))
819       player.run
820       begin
821         $mutex.lock
822         if (player.game)
823           player.game.finish
824         end
825         if (player.status != "finished")
826           player.finish
827         end
828         LEAGUE.delete(player)
829         log_message(sprintf("user %s logout", player.name))
830       ensure
831         $mutex.unlock
832       end
833     end
834   end
835 end
836
837 if ($0 == __FILE__)
838   main
839 end