OSDN Git Service

fix for CSA mode. game not started.
[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 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\s+(\S+)\s+([\+\-]{1,2})/
279           game_name = $1
280           sente_str = $2
281           if (! good_game_name?(game_name))
282             write_safe(sprintf("##[ERROR] bad game name\n"))
283             next
284           elsif ((@status == "connected") || (@status == "game_waiting"))
285             @status = "game_waiting"
286           else
287             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
288             next
289           end
290           @status = "game_waiting"
291           @game_name = $1
292           if (sente_str == "+")
293             @sente = true
294             rival_sente = false
295           elsif (sente_str == "-")
296             @sente = false
297             rival_sente = true
298           else
299             @sente = nil
300             rival_sente = nil
301           end
302           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
303           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
304           if (rival)
305             if (@sente == nil)
306               if (rand(2) == 0)
307                 @sente = true
308                 rival_sente = false
309               else
310                 @sente = false
311                 rival_sente = true
312               end
313             elsif (rival_sente == nil)
314               if (@sente)
315                 rival_sente = false
316               else
317                 rival_sente = true
318               end
319             end
320             rival.sente = rival_sente
321             Game::new(@game_name, self, rival)
322             self.status = "agree_waiting"
323             rival.status = "agree_waiting"
324           end
325         when /^%%CHAT\s+(.+)/
326           message = $1
327           LEAGUE.players.each do |name, player|
328             if (player.protocol != "CSA")
329               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
330               player.status = "zombie" if (! s)
331             end
332           end
333         when /^%%LIST/
334           buf = Array::new
335           LEAGUE.games.each do |id, game|
336             buf.push(sprintf("##[LIST] %s\n", id))
337           end
338           buf.push("##[LIST] +OK\n")
339           write_safe(buf.join)
340         when /^%%WHO/
341           buf = Array::new
342           LEAGUE.players.each do |name, player|
343             buf.push(sprintf("##[WHO] %s\n", player.to_s))
344           end
345           buf.push("##[WHO] +OK\n")
346           write_safe(buf.join)
347         when /^LOGOUT/
348           @status = "connected"
349           write_safe("LOGOUT:completed\n")
350           return
351         when /^\s*$/
352           ## ignore null string
353         else
354           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
355         end
356       ensure
357         $mutex.unlock
358       end
359     end                         # enf of while
360   end
361 end
362
363 class Piece
364   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
365   def initialize(name, sente)
366     @name = name
367     @sente = sente
368     @promoted = false
369   end
370   attr_accessor :name, :promoted, :sente
371
372   def promoted_name
373     PROMOTE[name]
374   end
375
376   def to_s
377     if (@sente)
378       sg = "+"
379     else
380       sg = "-"
381     end
382     if (@promoted)
383       n = PROMOTE[@name]
384     else
385       n = @name
386     end
387     return sg + n
388   end
389 end
390
391
392
393 class Board
394   def initialize
395     @sente_hands = Array::new
396     @gote_hands = Array::new
397     @history = Hash::new
398     @array = [[], [], [], [], [], [], [], [], [], []]
399   end
400   attr_accessor :array, :sente_hands, :gote_hands, :history
401
402   def initial
403     @array[1][1] = Piece::new("KY", false)
404     @array[2][1] = Piece::new("KE", false)
405     @array[3][1] = Piece::new("GI", false)
406     @array[4][1] = Piece::new("KI", false)
407     @array[5][1] = Piece::new("OU", false)
408     @array[6][1] = Piece::new("KI", false)
409     @array[7][1] = Piece::new("GI", false)
410     @array[8][1] = Piece::new("KE", false)
411     @array[9][1] = Piece::new("KY", false)
412     @array[2][2] = Piece::new("KA", false)
413     @array[8][2] = Piece::new("HI", false)
414     @array[1][3] = Piece::new("FU", false)
415     @array[2][3] = Piece::new("FU", false)
416     @array[3][3] = Piece::new("FU", false)
417     @array[4][3] = Piece::new("FU", false)
418     @array[5][3] = Piece::new("FU", false)
419     @array[6][3] = Piece::new("FU", false)
420     @array[7][3] = Piece::new("FU", false)
421     @array[8][3] = Piece::new("FU", false)
422     @array[9][3] = Piece::new("FU", false)
423
424     @array[1][9] = Piece::new("KY", true)
425     @array[2][9] = Piece::new("KE", true)
426     @array[3][9] = Piece::new("GI", true)
427     @array[4][9] = Piece::new("KI", true)
428     @array[5][9] = Piece::new("OU", true)
429     @array[6][9] = Piece::new("KI", true)
430     @array[7][9] = Piece::new("GI", true)
431     @array[8][9] = Piece::new("KE", true)
432     @array[9][9] = Piece::new("KY", true)
433     @array[2][8] = Piece::new("HI", true)
434     @array[8][8] = Piece::new("KA", true)
435     @array[1][7] = Piece::new("FU", true)
436     @array[2][7] = Piece::new("FU", true)
437     @array[3][7] = Piece::new("FU", true)
438     @array[4][7] = Piece::new("FU", true)
439     @array[5][7] = Piece::new("FU", true)
440     @array[6][7] = Piece::new("FU", true)
441     @array[7][7] = Piece::new("FU", true)
442     @array[8][7] = Piece::new("FU", true)
443     @array[9][7] = Piece::new("FU", true)
444   end
445
446   def have_piece?(hands, name)
447     p = hands.find { |i|
448       i.name == name
449     }
450     return p
451   end
452
453   def get_piece_from_hands(hands, name)
454     p = hands.find { |i|
455       i.name == name
456     }
457     if (p)
458       hands.delete(p)
459     end
460     return p
461   end
462
463   def handle_one_move(str)
464     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
465       p = $1
466       x0 = $2.to_i
467       y0 = $3.to_i
468       x1 = $4.to_i
469       y1 = $5.to_i
470       name = $6
471     elsif (str =~ /^%KACHI/)
472       return :kachi
473     elsif (str =~ /^%TORYO/)
474       return :toryo
475     else
476       return :illegal
477     end
478     
479     if (((x0 == 0) || (y0 == 0)) && # source is not from hand
480         ((x0 != 0) || (y0 != 0)))
481       return :illegal
482     elsif ((x1 == 0) || (y1 == 0)) # destination is out of board
483       return :illegal
484     end
485     
486     if (p == "+")
487       sente = true
488       hands = @sente_hands
489     else
490       sente = false
491       hands = @gote_hands
492     end
493     
494     ## source check
495     if ((x0 == 0) && (y0 == 0))
496       return :illegal if (! have_piece?(hands, name))
497     elsif (! @array[x0][y0])
498       return :illegal           # no piece
499     elsif (@array[x0][y0].sente != sente)
500       return :illegal           # this is not mine
501     elsif (@array[x0][y0].name != name)
502       return :illegal if (@array[x0][y0].promoted_name != name) # can't promote
503     end
504     
505     ## destination check
506     if (@array[x1][y1] &&
507         (@array[x1][y1].sente == sente)) # can't capture mine
508       return :illegal
509     elsif ((x0 == 0) && (y0 == 0) && @array[x1][y1])
510       return :illegal           # can't put on existing piece
511     end
512
513     if ((x0 == 0) && (y0 == 0))
514       p = get_piece_from_hands(hands, name)
515       @array[x1][y1] = p
516       p.sente = sente
517       p.promoted = false
518     else
519       if (@array[x0][y0].name != name) # promoted ?
520         @array[x0][y0].promoted = true
521       end
522       if (@array[x1][y1])
523         if (@array[x1][y1].name == "OU")
524           return :outori        # return board update
525         end
526         hands.push(@array[x1][y1])
527         hands.sort! {|a, b|
528           a.name <=> b.name
529         }
530       end
531       @array[x1][y1] = @array[x0][y0]
532       @array[x0][y0] = nil
533     end
534
535     ## sennichite check
536     str = to_s
537     @history[str] = (@history[str] || 0) + 1
538     if (@history[str] >= 4)
539       return :sennichite
540     end
541
542     return :normal             # legal move
543   end
544
545   def to_s
546     a = Array::new
547     y = 1
548     while (y <= 9)
549       a.push(sprintf("P%d", y))
550       x = 9
551       while (x >= 1)
552         piece = @array[x][y]
553         if (piece)
554           s = piece.to_s
555         else
556           s = " * "
557         end
558         a.push(s)
559         x = x - 1
560       end
561       a.push(sprintf("\n"))
562       y = y + 1
563     end
564     if (! sente_hands.empty?)
565       a.push("P+")
566       sente_hands.each do |p|
567         a.push("00" + p.name)
568       end
569       a.push("\n")
570     end
571     if (! gote_hands.empty?)
572       a.push("P-")
573       gote_hands.each do |p|
574         a.push("00" + p.name)
575       end
576       a.push("\n")
577     end
578     a.push("+\n")
579     return a.join
580   end
581 end
582
583 class Game
584   def initialize(game_name, player0, player1)
585     @monitors = Array::new
586     @game_name = game_name
587     if (@game_name =~ /-(\d+)-(\d+)$/)
588       @total_time = $1.to_i
589       @byoyomi = $2.to_i
590     end
591
592     if (player0.sente)
593       @sente = player0
594       @gote = player1
595     else
596       @sente = player1
597       @gote = player0
598     end
599     @current_player = @sente
600     @next_player = @gote
601
602     @sente.game = self
603     @gote.game = self
604
605     @last_move = ""
606     @current_turn = 0
607
608     @sente.status = "agree_waiting"
609     @gote.status = "agree_waiting"
610     @id = sprintf("%s+%s+%s+%s+%s", 
611                   LEAGUE.event, @game_name, @sente.name, @gote.name,
612                   Time::new.strftime("%Y%m%d%H%M%S"))
613
614     LEAGUE.games[@id] = self
615
616
617     log_message(sprintf("game created %s", @id))
618
619     @logfile = @id + ".csa"
620     @board = Board::new
621     @board.initial
622     @start_time = nil
623     @fh = nil
624
625     propose
626   end
627   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
628   attr_accessor :last_move, :current_turn
629
630   def monitoron(monitor)
631     @monitors.delete(monitor)
632     @monitors.push(monitor)
633   end
634
635   def monitoroff(monitor)
636     @monitors.delete(monitor)
637   end
638
639   def reject(rejector)
640     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
641     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
642     finish
643   end
644
645   def kill(killer)
646     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
647       reject(killer.name)
648     elsif (@current_player == killer)
649       abnormal_lose()
650       finish
651     end
652   end
653
654   def finish
655     log_message(sprintf("game finished %s", @id))
656     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
657     @fh.close
658
659     @sente.game = nil
660     @gote.game = nil
661     @sente.status = "connected"
662     @gote.status = "connected"
663
664     if (@current_player.protocol == "CSA")
665       @current_player.finish
666     end
667     if (@next_player.protocol == "CSA")
668       @next_player.finish
669     end
670     @monitors = Array::new
671     @sente = nil
672     @gote = nil
673     @current_player = nil
674     @next_player = nil
675     LEAGUE.games.delete(@id)
676   end
677
678   def handle_one_move(str, player)
679     finish_flag = true
680     if (@current_player == player)
681       @end_time = Time::new
682       t = (@end_time - @start_time).ceil
683       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
684       
685       move_status = nil
686       if ((@current_player.mytime - t <= 0) && (@total_time > 0))
687         status = :timeout
688       elsif (str == :timeout)
689         return false            # time isn't expired. players aren't swapped. continue game
690       else
691         begin
692           move_status = @board.handle_one_move(str)
693         rescue
694           log_error("handle_one_move raise exception for #{str}")
695           move_status = :illegal
696         end
697         if (move_status == :illegal)
698           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
699         else
700           if ((move_status == :normal) || (move_status == :outori) || (move_status == :sennichite))
701             @sente.write_safe(sprintf("%s,T%d\n", str, t))
702             @gote.write_safe(sprintf("%s,T%d\n", str, t))
703             @fh.printf("%s\nT%d\n", str, t)
704             @last_move = sprintf("%s,T%d", str, t)
705             @current_turn = @current_turn + 1
706           end
707           @monitors.each do |monitor|
708             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
709             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
710           end
711         end
712       end
713
714       if (@current_player.mytime - t < @byoyomi)
715         @current_player.mytime = @byoyomi
716       else
717         @current_player.mytime = @current_player.mytime - t
718       end
719
720       if (@next_player.status != "game") # rival is logout or disconnected
721         abnormal_win()
722       elsif (status == :timeout)
723         timeout_lose()
724       elsif (move_status == :illegal)
725         illegal_lose()
726       elsif (move_status == :kachi)
727         kachi_win()
728       elsif (move_status == :toryo)
729         toryo_lose()
730       elsif (move_status == :outori)
731         outori_win()
732       elsif (move_status == :sennichite)
733         sennichite_draw()
734       else
735         finish_flag = false
736       end
737       finish() if finish_flag
738       (@current_player, @next_player) = [@next_player, @current_player]
739       @start_time = Time::new
740       return finish_flag
741     end
742   end
743
744   def abnormal_win
745     @current_player.status = "connected"
746     @next_player.status = "connected"
747     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
748     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
749     @fh.printf("%%TORYO\n")
750     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
751     @monitors.each do |monitor|
752       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
753     end
754   end
755
756   def abnormal_lose
757     @current_player.status = "connected"
758     @next_player.status = "connected"
759     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
760     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
761     @fh.printf("%%TORYO\n")
762     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
763     @monitors.each do |monitor|
764       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
765     end
766   end
767
768   def sennichite_draw
769     @current_player.status = "connected"
770     @next_player.status = "connected"
771     @current_player.write_safe("#SENNICHITE\n#DRAW\n")
772     @next_player.write_safe("#SENNICHITE\n#DRAW\n")
773     @fh.printf("'summary:sennichite:%s draw:%s draw\n", @current_player.name, @next_player.name)
774     @monitors.each do |monitor|
775       monitor.write_safe(sprintf("##[MONITOR][%s] #SENNICHITE\n", @id))
776     end
777   end
778
779   def illegal_lose
780     @current_player.status = "connected"
781     @next_player.status = "connected"
782     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
783     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
784     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
785     @monitors.each do |monitor|
786       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
787     end
788   end
789
790   def timeout_lose
791     @current_player.status = "connected"
792     @next_player.status = "connected"
793     @current_player.write_safe("#TIME_UP\n#LOSE\n")
794     @next_player.write_safe("#TIME_UP\n#WIN\n")
795     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
796     @monitors.each do |monitor|
797       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
798     end
799   end
800
801   def kachi_win
802     @current_player.status = "connected"
803     @next_player.status = "connected"
804     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
805     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
806     @fh.printf("%%KACHI\n")
807     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
808     @monitors.each do |monitor|
809       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
810     end
811   end
812
813   def toryo_lose
814     @current_player.status = "connected"
815     @next_player.status = "connected"
816     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
817     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
818     @fh.printf("%%TORYO\n")
819     @fh.printf("'summary:toryo:%s lose:%s win\n", @current_player.name, @next_player.name)
820     @monitors.each do |monitor|
821       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
822     end
823   end
824
825   def outori_win
826     @current_player.status = "connected"
827     @next_player.status = "connected"
828     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
829     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
830     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
831     @monitors.each do |monitor|
832       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
833     end
834   end
835
836   def start
837     log_message(sprintf("game started %s", @id))
838     @sente.write_safe(sprintf("START:%s\n", @id))
839     @gote.write_safe(sprintf("START:%s\n", @id))
840     @sente.mytime = @total_time
841     @gote.mytime = @total_time
842     @start_time = Time::new
843   end
844
845   def propose
846     begin
847       @fh = open(@logfile, "w")
848       @fh.sync = true
849
850       @fh.printf("V2\n")
851       @fh.printf("N+%s\n", @sente.name)
852       @fh.printf("N-%s\n", @gote.name)
853       @fh.printf("$EVENT:%s\n", @id)
854
855       @sente.write_safe(propose_message("+"))
856       @gote.write_safe(propose_message("-"))
857
858       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
859       @fh.print <<EOM
860 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
861 P2 * -HI *  *  *  *  * -KA * 
862 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
863 P4 *  *  *  *  *  *  *  *  * 
864 P5 *  *  *  *  *  *  *  *  * 
865 P6 *  *  *  *  *  *  *  *  * 
866 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
867 P8 * +KA *  *  *  *  * +HI * 
868 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
869 +
870 EOM
871     end
872   end
873
874   def show()
875     str0 = <<EOM
876 BEGIN Game_Summary
877 Protocol_Version:1.0
878 Protocol_Mode:Server
879 Format:Shogi 1.0
880 Game_ID:#{@id}
881 Name+:#{@sente.name}
882 Name-:#{@gote.name}
883 Rematch_On_Draw:NO
884 To_Move:+
885 BEGIN Time
886 Time_Unit:1sec
887 Total_Time:#{@total_time}
888 Byoyomi:#{@byoyomi}
889 Least_Time_Per_Move:#{Least_Time_Per_Move}
890 Remaining_Time+:#{@sente.mytime}
891 Remaining_Time-:#{@gote.mytime}
892 Last_Move:#{@last_move}
893 Current_Turn:#{@current_turn}
894 END Time
895 BEGIN Position
896 Jishogi_Declaration:1.1
897 EOM
898
899     str1 = <<EOM
900 END Position
901 END Game_Summary
902 EOM
903
904     return str0 + @board.to_s + str1
905   end
906
907   def propose_message(sg_flag)
908     str = <<EOM
909 BEGIN Game_Summary
910 Protocol_Version:1.0
911 Protocol_Mode:Server
912 Format:Shogi 1.0
913 Game_ID:#{@id}
914 Name+:#{@sente.name}
915 Name-:#{@gote.name}
916 Your_Turn:#{sg_flag}
917 Rematch_On_Draw:NO
918 To_Move:+
919 BEGIN Time
920 Time_Unit:1sec
921 Total_Time:#{@total_time}
922 Byoyomi:#{@byoyomi}
923 Least_Time_Per_Move:#{Least_Time_Per_Move}
924 END Time
925 BEGIN Position
926 Jishogi_Declaration:1.1
927 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
928 P2 * -HI *  *  *  *  * -KA * 
929 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
930 P4 *  *  *  *  *  *  *  *  * 
931 P5 *  *  *  *  *  *  *  *  * 
932 P6 *  *  *  *  *  *  *  *  * 
933 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
934 P8 * +KA *  *  *  *  * +HI * 
935 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
936 P+
937 P-
938 +
939 END Position
940 END Game_Summary
941 EOM
942     return str
943   end
944 end
945
946 def usage
947     print <<EOM
948 NAME
949         shogi-server - server for CSA server protocol
950
951 SYNOPSIS
952         shogi-server event_name port_number
953
954 DESCRIPTION
955         server for CSA server protocol
956
957 OPTIONS
958         --pid-file file
959                 specify filename for logging process ID
960
961 LICENSE
962         this file is distributed under GPL version2 and might be compiled by Exerb
963
964 SEE ALSO
965
966 RELEASE
967         #{Release}
968
969 REVISION
970         #{Revision}
971 EOM
972 end
973
974 def log_message(str)
975   printf("%s message: %s\n", Time::new.to_s, str)
976 end
977
978 def log_warning(str)
979   printf("%s warning: %s\n", Time::new.to_s, str)
980 end
981
982 def log_error(str)
983   printf("%s error: %s\n", Time::new.to_s, str)
984 end
985
986
987 def parse_command_line
988   options = Hash::new
989   parser = GetoptLong.new
990   parser.ordering = GetoptLong::REQUIRE_ORDER
991   parser.set_options(
992                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
993
994   parser.quiet = true
995   begin
996     parser.each_option do |name, arg|
997       name.sub!(/^--/, '')
998       options[name] = arg.dup
999     end
1000   rescue
1001     usage
1002     raise parser.error_message
1003   end
1004   return options
1005 end
1006
1007 def good_game_name?(str)
1008   if ((str =~ /^(.+)-\d+-\d+$/) &&
1009       (good_identifier?($1)))
1010     return true
1011   else
1012     return false
1013   end
1014 end
1015
1016 def good_identifier?(str)
1017   if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
1018       (str.length < Max_Identifier_Length))
1019     return true
1020   else
1021     return false
1022   end
1023 end
1024
1025 def good_login?(str)
1026   tokens = str.split
1027   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
1028       (tokens[0] == "LOGIN") &&
1029       (good_identifier?(tokens[1])))
1030     return true
1031   else
1032     return false
1033   end
1034 end
1035
1036 def  write_pid_file(file)
1037   open(file, "w") do |fh|
1038     fh.print Process::pid, "\n"
1039   end
1040 end
1041
1042 def mutex_watchdog(mutex, sec)
1043   while true
1044     begin
1045       timeout(sec) do
1046         mutex.lock
1047         mutex.unlock
1048       end
1049       sleep(sec)
1050     rescue TimeoutError
1051       log_error("mutex watchdog timeout")
1052       exit(1)
1053     end
1054   end
1055 end
1056
1057 def main
1058   $mutex = Mutex::new
1059   Thread::start do
1060     mutex_watchdog($mutex, 10)
1061   end
1062
1063   $options = parse_command_line
1064   if (ARGV.length != 2)
1065     usage
1066     exit 2
1067   end
1068
1069   LEAGUE.event = ARGV.shift
1070   port = ARGV.shift
1071
1072   write_pid_file($options["pid-file"]) if ($options["pid-file"])
1073
1074
1075   Thread.abort_on_exception = true
1076
1077   server = TCPserver.open(port)
1078   log_message("server started")
1079
1080   while true
1081     Thread::start(server.accept) do |client|
1082       client.sync = true
1083       player = nil
1084       while (str = client.gets_timeout(Login_Time))
1085         begin
1086           $mutex.lock
1087           str =~ /([\r\n]*)$/
1088           eol = $1
1089           if (good_login?(str))
1090             player = Player::new(str, client)
1091             if (LEAGUE.players[player.name])
1092               if ((LEAGUE.players[player.name].password == player.password) &&
1093                   (LEAGUE.players[player.name].status != "game"))
1094                 log_message(sprintf("user %s login forcely", player.name))
1095                 LEAGUE.players[player.name].kill
1096               else
1097                 client.write_safe("LOGIN:incorrect" + eol)
1098                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1099                 client.close
1100                 Thread::exit
1101               end
1102             end
1103             LEAGUE.add(player)
1104             break
1105           else
1106             client.write_safe("LOGIN:incorrect" + eol)
1107             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1108           end
1109         ensure
1110           $mutex.unlock
1111         end
1112       end                       # login loop
1113       if (! player)
1114         client.close
1115         Thread::exit
1116       end
1117       log_message(sprintf("user %s login", player.name))
1118       player.run
1119       begin
1120         $mutex.lock
1121         if (player.game)
1122           player.game.kill(player)
1123         end
1124         player.finish
1125         LEAGUE.delete(player)
1126         log_message(sprintf("user %s logout", player.name))
1127       ensure
1128         $mutex.unlock
1129       end
1130     end
1131   end
1132 end
1133
1134 if ($0 == __FILE__)
1135   LEAGUE = League::new
1136   main
1137 end