OSDN Git Service

992c5d83f8b88b9c57f9653d47a018dbd6d871a0
[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 require 'ping'
42
43 TCPSocket.do_not_reverse_lookup = true
44
45
46 class TCPSocket
47   def gets_timeout(t = Default_Timeout)
48     begin
49       timeout(t) do
50         return self.gets
51       end
52     rescue TimeoutError
53       return nil
54     rescue
55       return nil
56     end
57   end
58   def gets_safe(t = nil)
59     if (t && t > 0)
60       begin
61         timeout(t) do
62           return self.gets
63         end
64       rescue TimeoutError
65         return :timeout
66       rescue
67         return nil
68       end
69     else
70       begin
71         return self.gets
72       rescue
73         return nil
74       end
75     end
76   end
77   def write_safe(str)
78     begin
79       return self.write(str)
80     rescue
81       return nil
82     end
83   end
84 end
85
86
87 class League
88   def initialize
89     @games = Hash::new
90     @players = Hash::new
91     @event = nil
92   end
93   attr_accessor :players, :games, :event
94
95   def add(player)
96     @players[player.name] = player
97   end
98   def delete(player)
99     @players.delete(player.name)
100   end
101   def get_player(status, game_name, sente, searcher=nil)
102     @players.each do |name, player|
103       if ((player.status == status) &&
104           (player.game_name == game_name) &&
105           ((player.sente == nil) || (player.sente == sente)) &&
106           ((searcher == nil) || (player != searcher)))
107         return player
108       end
109     end
110     return nil
111   end
112 end
113
114 class Player
115   def initialize(str, socket)
116     @name = nil
117     @password = nil
118     @socket = socket
119     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game -> finished
120
121     @protocol = nil             # CSA or x1
122     @eol = "\m"                 # favorite eol code
123     @game = nil
124     @game_name = ""
125     @mytime = 0                 # set in start method also
126     @sente = nil
127     @writer_thread = nil
128     @main_thread = nil
129     @write_queue = Queue::new
130     login(str)
131   end
132
133   attr_accessor :name, :password, :socket, :status
134   attr_accessor :protocol, :eol, :game, :mytime, :game_name, :sente
135   attr_accessor :main_thread, :writer_thread, :write_queue
136   def kill
137     finish
138     Thread::kill(@main_thread) if @main_thread
139   end
140
141   def finish
142     if (@status != "finished")
143       @status = "finished"
144       log_message(sprintf("user %s finish", @name))    
145       Thread::kill(@writer_thread) if @writer_thread
146       begin
147         @socket.close if (! @socket.closed?)
148       rescue
149         log_message(sprintf("user %s finish failed", @name))    
150       end
151     end
152   end
153
154   def write_safe(str)
155     @write_queue.push(str.gsub(/[\r\n]+/, @eol))
156   end
157
158   def writer
159     while (str = @write_queue.pop)
160       @socket.write_safe(str)
161     end
162   end
163
164   def to_s
165     if ((status == "game_waiting") ||
166         (status == "agree_waiting") ||
167         (status == "game"))
168       if (@sente)
169         return sprintf("%s %s %s %s +", @name, @protocol, @status, @game_name)
170       elsif (@sente == false)
171         return sprintf("%s %s %s %s -", @name, @protocol, @status, @game_name)
172       elsif (@sente == nil)
173         return sprintf("%s %s %s %s +-", @name, @protocol, @status, @game_name)
174       end
175     else
176       return sprintf("%s %s %s", @name, @protocol, @status)
177     end
178   end
179
180   def write_help
181     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
182   end
183
184   def login(str)
185     str =~ /([\r\n]*)$/
186     @eol = $1
187     str.chomp!
188     (login, @name, @password, ext) = str.split
189     if (ext)
190       @protocol = "x1"
191     else
192       @protocol = "CSA"
193     end
194     @main_thread = Thread::current
195     @writer_thread = Thread::start do
196       writer()
197     end
198   end
199     
200   def run
201     write_safe(sprintf("LOGIN:%s OK\n", @name))
202     if (@protocol != "CSA")
203       log_message(sprintf("user %s run in %s mode", @name, @protocol))
204       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
205     else
206       log_message(sprintf("user %s run in CSA mode", @name))
207       csa_1st_str = "%%GAME #{Default_Game_Name} +-"
208     end
209     
210     while (csa_1st_str || (str = @socket.gets_safe(Default_Timeout)))
211       begin
212         $mutex.lock
213         if (csa_1st_str)
214           str = csa_1st_str
215           csa_1st_str = nil
216         end
217         if (@write_queue.size > Max_Write_Queue_Size)
218           log_warning(sprintf("write_queue of %s is %d", @name, @write_queue.size))
219           return
220         end
221
222         if (@status == "finished")
223           return
224         end
225         str.chomp! if (str.class == String)
226         case str
227         when /^[\+\-%][^%]/, :timeout
228           if (@status == "game")
229             s = @game.handle_one_move(str, self)
230             return if (s && @protocol == "CSA")
231           end
232         when /^REJECT/
233           if (@status == "agree_waiting")
234             @game.reject(@name)
235             return if (@protocol == "CSA")
236           else
237             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
238           end
239         when /^AGREE/
240           if (@status == "agree_waiting")
241             @status = "start_waiting"
242             if ((@game.sente.status == "start_waiting") &&
243                 (@game.gote.status == "start_waiting"))
244               @game.start
245               @game.sente.status = "game"
246               @game.gote.status = "game"
247             end
248           else
249             write_safe(sprintf("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status))
250           end
251         when /^%%SHOW\s+(\S+)/
252           game_id = $1
253           if (LEAGUE.games[game_id])
254             write_safe(LEAGUE.games[game_id].show.gsub(/^/, '##[SHOW] '))
255           end
256           write_safe("##[SHOW] +OK\n")
257         when /^%%MONITORON\s+(\S+)/
258           game_id = $1
259           if (LEAGUE.games[game_id])
260             LEAGUE.games[game_id].monitoron(self)
261             write_safe(LEAGUE.games[game_id].show.gsub(/^/, "##[MONITOR][#{game_id}] "))
262             write_safe("##[MONITOR][#{game_id}] +OK\n")
263           end
264         when /^%%MONITOROFF\s+(\S+)/
265           game_id = $1
266           if (LEAGUE.games[game_id])
267             LEAGUE.games[game_id].monitoroff(self)
268           end
269         when /^%%HELP/
270           write_help
271         when /^%%GAME\s+(\S+)\s+([\+\-]+)$/
272           game_name = $1
273           sente_str = $2
274           if (! good_game_name?(game_name))
275             write_safe(sprintf("##[ERROR] bad game name\n"))
276           elsif ((@status == "connected") || (@status == "game_waiting"))
277             @status = "game_waiting"
278           else
279             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
280           end
281           @status = "game_waiting"
282           @game_name = $1
283           if (sente_str == "+")
284             @sente = true
285             rival_sente = false
286           elsif (sente_str == "-")
287             @sente = false
288             rival_sente = true
289           else
290             @sente = nil
291             rival_sente = nil
292           end
293           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
294           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
295           if (rival)
296             if (@sente == nil)
297               if (rand(2) == 0)
298                 @sente = true
299                 rival_sente = false
300               else
301                 @sente = false
302                 rival_sente = true
303               end
304             elsif (rival_sente == nil)
305               if (@sente)
306                 rival_sente = false
307               else
308                 rival_sente = true
309               end
310             end
311             rival.sente = rival_sente
312             Game::new(@game_name, self, rival)
313             self.status = "agree_waiting"
314             rival.status = "agree_waiting"
315           end
316         when /^%%CHAT\s+(.+)/
317           message = $1
318           LEAGUE.players.each do |name, player|
319             if (player.protocol != "CSA")
320               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
321               player.status = "zombie" if (! s)
322             end
323           end
324         when /^%%LIST/
325           buf = Array::new
326           LEAGUE.games.each do |id, game|
327             buf.push(sprintf("##[LIST] %s\n", id))
328           end
329           buf.push("##[LIST] +OK\n")
330           write_safe(buf.join)
331         when /^%%WHO/
332           buf = Array::new
333           LEAGUE.players.each do |name, player|
334             buf.push(sprintf("##[WHO] %s\n", player.to_s))
335           end
336           buf.push("##[WHO] +OK\n")
337           write_safe(buf.join)
338         when /^LOGOUT/
339           @status = "connected"
340           write_safe("LOGOUT:completed\n")
341           return
342         when /^\s*$/
343           ## ignore null string
344         else
345           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
346         end
347       ensure
348         $mutex.unlock
349       end
350     end                         # enf of while
351   end
352 end
353
354 class Piece
355   PROMOTE = {"FU" => "TO", "KY" => "NY", "KE" => "NK", "GI" => "NG", "KA" => "UM", "HI" => "RY"}
356   def initialize(name, sente)
357     @name = name
358     @sente = sente
359     @promoted = false
360   end
361   attr_accessor :name, :promoted, :sente
362
363   def promoted_name
364     PROMOTE[name]
365   end
366
367   def to_s
368     if (@sente)
369       sg = "+"
370     else
371       sg = "-"
372     end
373     if (@promoted)
374       n = PROMOTE[@name]
375     else
376       n = @name
377     end
378     return sg + n
379   end
380 end
381
382
383
384 class Board
385   def initialize
386     @sente_hands = Array::new
387     @gote_hands = Array::new
388     @array = [[], [], [], [], [], [], [], [], [], []]
389   end
390   attr_accessor :array, :sente_hands, :gote_hands
391
392   def initial
393     @array[1][1] = Piece::new("KY", false)
394     @array[2][1] = Piece::new("KE", false)
395     @array[3][1] = Piece::new("GI", false)
396     @array[4][1] = Piece::new("KI", false)
397     @array[5][1] = Piece::new("OU", false)
398     @array[6][1] = Piece::new("KI", false)
399     @array[7][1] = Piece::new("GI", false)
400     @array[8][1] = Piece::new("KE", false)
401     @array[9][1] = Piece::new("KY", false)
402     @array[2][2] = Piece::new("KA", false)
403     @array[8][2] = Piece::new("HI", false)
404     @array[1][3] = Piece::new("FU", false)
405     @array[2][3] = Piece::new("FU", false)
406     @array[3][3] = Piece::new("FU", false)
407     @array[4][3] = Piece::new("FU", false)
408     @array[5][3] = Piece::new("FU", false)
409     @array[6][3] = Piece::new("FU", false)
410     @array[7][3] = Piece::new("FU", false)
411     @array[8][3] = Piece::new("FU", false)
412     @array[9][3] = Piece::new("FU", false)
413
414     @array[1][9] = Piece::new("KY", true)
415     @array[2][9] = Piece::new("KE", true)
416     @array[3][9] = Piece::new("GI", true)
417     @array[4][9] = Piece::new("KI", true)
418     @array[5][9] = Piece::new("OU", true)
419     @array[6][9] = Piece::new("KI", true)
420     @array[7][9] = Piece::new("GI", true)
421     @array[8][9] = Piece::new("KE", true)
422     @array[9][9] = Piece::new("KY", true)
423     @array[2][8] = Piece::new("HI", true)
424     @array[8][8] = Piece::new("KA", true)
425     @array[1][7] = Piece::new("FU", true)
426     @array[2][7] = Piece::new("FU", true)
427     @array[3][7] = Piece::new("FU", true)
428     @array[4][7] = Piece::new("FU", true)
429     @array[5][7] = Piece::new("FU", true)
430     @array[6][7] = Piece::new("FU", true)
431     @array[7][7] = Piece::new("FU", true)
432     @array[8][7] = Piece::new("FU", true)
433     @array[9][7] = Piece::new("FU", true)
434   end
435
436   def get_piece_from_hands(hands, name)
437     p = hands.find { |i|
438       i.name == name
439     }
440     if (p)
441       hands.delete(p)
442     end
443     return p
444   end
445
446   def handle_one_move(str)
447   begin
448     if (str =~ /^([\+\-])(\d)(\d)(\d)(\d)([A-Z]{2})/)
449       p = $1
450       x0 = $2.to_i
451       y0 = $3.to_i
452       x1 = $4.to_i
453       y1 = $5.to_i
454       name = $6
455
456       if ((x1 == 0) || (y1 == 0))
457         return "illegal"
458       end
459     elsif (str =~ /^%KACHI/)
460       return "kachi"
461     elsif (str =~ /^%TORYO/)
462       return "toryo"
463     else
464       return "illegal"
465     end
466
467     if (p == "+")
468       sente = true
469       hands = @sente_hands
470     else
471       sente = false
472       hands = @gote_hands
473     end
474     if (@array[x1][y1])
475       if (@array[x1][y1] == sente) # this is mine
476         return "illegal"
477       elsif (@array[x1][y1].name == "OU")
478         return "outori"
479       end
480       hands.push(@array[x1][y1])
481       @array[x1][y1] = nil
482     end
483     if ((x0 == 0) && (y0 == 0))
484       p = get_piece_from_hands(hands, name)
485       return "illegal" if (! p)     # i don't have this one
486       @array[x1][y1] = p
487       p.sente = sente
488       p.promoted = false
489     else
490       if (@array[x0][y0] == nil)
491         return "illegal";
492       end
493       if (@array[x0][y0].name != name) # promoted ?
494         return "illegal" if (@array[x0][y0].promoted_name != name) # can't promote
495         @array[x0][y0].promoted = true
496       end
497       @array[x1][y1] = @array[x0][y0]
498       @array[x0][y0] = nil
499     end
500     return "normal"             # legal move
501   rescue
502     return "illegal"
503   end
504   end
505
506   def to_s
507     a = Array::new
508     y = 1
509     while (y <= 9)
510       a.push(sprintf("P%d", y))
511       x = 9
512       while (x >= 1)
513         piece = @array[x][y]
514         if (piece)
515           s = piece.to_s
516         else
517           s = " * "
518         end
519         a.push(s)
520         x = x - 1
521       end
522       a.push(sprintf("\n"))
523       y = y + 1
524     end
525     if (! sente_hands.empty?)
526       a.push("P+")
527       sente_hands.each do |p|
528         a.push("00" + p.name)
529       end
530       a.push("\n")
531     end
532     if (! gote_hands.empty?)
533       a.push("P-")
534       gote_hands.each do |p|
535         a.push("00" + p.name)
536       end
537       a.push("\n")
538     end
539     a.push("+\n")
540     return a.join
541   end
542 end
543
544 class Game
545   def initialize(game_name, player0, player1)
546     @monitors = Array::new
547     @game_name = game_name
548     if (@game_name =~ /:(\d+):(\d+)/)
549       @total_time = $1.to_i
550       @byoyomi = $2.to_i
551     end
552
553     if (player0.sente)
554       @sente = player0
555       @gote = player1
556     else
557       @sente = player1
558       @gote = player0
559     end
560     @current_player = @sente
561     @next_player = @gote
562
563     @sente.game = self
564     @gote.game = self
565
566     @last_move = ""
567     @current_turn = 0
568
569     @sente.status = "agree_waiting"
570     @gote.status = "agree_waiting"
571     @id = sprintf("%s+%s+%s+%s+%s", 
572                   LEAGUE.event, @game_name, @sente.name, @gote.name,
573                   Time::new.strftime("%Y%m%d%H%M%S"))
574
575     LEAGUE.games[@id] = self
576
577
578     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
579
580     @logfile = @id + ".csa"
581     @board = Board::new
582     @board.initial
583     @start_time = nil
584     @fh = nil
585
586     propose
587   end
588   attr_accessor :game_name, :total_time, :byoyomi, :sente, :gote, :id, :board, :current_player, :next_player, :fh, :monitors
589   attr_accessor :last_move, :current_turn
590
591   def monitoron(monitor)
592     @monitors.delete(monitor)
593     @monitors.push(monitor)
594   end
595
596   def monitoroff(monitor)
597     @monitors.delete(monitor)
598   end
599
600   def reject(rejector)
601     @sente.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
602     @gote.write_safe(sprintf("REJECT:%s by %s\n", @id, rejector))
603     finish
604   end
605
606   def kill(killer)
607     if ((@sente.status == "agree_waiting") || (@sente.status == "start_waiting"))
608       reject(killer.name)
609     elsif (@current_player == killer)
610       abnormal_lose()
611       finish
612     end
613   end
614
615   def finish
616     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
617     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
618     @fh.close
619
620     @sente.game = nil
621     @gote.game = nil
622     @sente.status = "connected"
623     @gote.status = "connected"
624
625     if (@current_player.protocol == "CSA")
626       @current_player.finish
627     end
628     if (@next_player.protocol == "CSA")
629       @next_player.finish
630     end
631     @monitors = Array::new
632     @sente = nil
633     @gote = nil
634     @current_player = nil
635     @next_player = nil
636     LEAGUE.games.delete(@id)
637   end
638
639   def handle_one_move(str, player)
640     finish_flag = true
641     if (@current_player == player)
642       @end_time = Time::new
643       t = (@end_time - @start_time).ceil
644       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
645       
646       move_status = nil
647       if ((@current_player.mytime - t <= 0) && (@total_time > 0))
648         status = :timeout
649       elsif (str == :timeout)
650         return false            # time isn't expired. players aren't swapped. continue game
651       else
652         move_status = @board.handle_one_move(str)
653         if (move_status == "illegal")
654           @fh.printf("'ILLEGAL_MOVE(%s)\n", str)
655         else
656           if ((move_status == "normal") || (move_status == "outori"))
657             @sente.write_safe(sprintf("%s,T%d\n", str, t))
658             @gote.write_safe(sprintf("%s,T%d\n", str, t))
659             @fh.printf("%s\nT%d\n", str, t)
660             @last_move = sprintf("%s,T%d", str, t)
661             @current_turn = @current_turn + 1
662           end
663           @monitors.each do |monitor|
664             monitor.write_safe(show.gsub(/^/, "##[MONITOR][#{@id}] "))
665             monitor.write_safe(sprintf("##[MONITOR][%s] +OK\n", @id))
666           end
667         end
668       end
669
670       if (@current_player.mytime - t < @byoyomi)
671         @current_player.mytime = @byoyomi
672       else
673         @current_player.mytime = @current_player.mytime - t
674       end
675
676       if (@next_player.status != "game") # rival is logout or disconnected
677         abnormal_win()
678       elsif (status == :timeout)
679         timeout_lose()
680       elsif (move_status == "illegal")
681         illegal_lose()
682       elsif (move_status == "kachi")
683         kachi_win()
684       elsif (move_status == "toryo")
685         toryo_lose()
686       elsif (move_status == "outori")
687         outori_win()
688       else
689         finish_flag = false
690       end
691       finish() if finish_flag
692       (@current_player, @next_player) = [@next_player, @current_player]
693       @start_time = Time::new
694       return finish_flag
695     end
696   end
697
698   def abnormal_win
699     @current_player.status = "connected"
700     @next_player.status = "connected"
701     @current_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
702     @next_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
703     @fh.printf("%%TORYO\n")
704     @fh.printf("'summary:abnormal:%s win:%s lose\n", @current_player.name, @next_player.name)
705     @monitors.each do |monitor|
706       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
707     end
708   end
709
710   def abnormal_lose
711     @current_player.status = "connected"
712     @next_player.status = "connected"
713     @current_player.write_safe("%TORYO\n#RESIGN\n#LOSE\n")
714     @next_player.write_safe("%TORYO\n#RESIGN\n#WIN\n")
715     @fh.printf("%%TORYO\n")
716     @fh.printf("'summary:abnormal:%s lose:%s win\n", @current_player.name, @next_player.name)
717     @monitors.each do |monitor|
718       monitor.write_safe(sprintf("##[MONITOR][%s] %%TORYO\n", @id))
719     end
720   end
721
722   def illegal_lose
723     @current_player.status = "connected"
724     @next_player.status = "connected"
725     @current_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
726     @next_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
727     @fh.printf("'summary:illegal move:%s lose:%s win\n", @current_player.name, @next_player.name)
728     @monitors.each do |monitor|
729       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
730     end
731   end
732
733   def timeout_lose
734     @current_player.status = "connected"
735     @next_player.status = "connected"
736     @current_player.write_safe("#TIME_UP\n#LOSE\n")
737     @next_player.write_safe("#TIME_UP\n#WIN\n")
738     @fh.printf("'summary:time up:%s lose:%s win\n", @current_player.name, @next_player.name)
739     @monitors.each do |monitor|
740       monitor.write_safe(sprintf("##[MONITOR][%s] #TIME_UP\n", @id))
741     end
742   end
743
744   def kachi_win
745     @current_player.status = "connected"
746     @next_player.status = "connected"
747     @current_player.write_safe("%KACHI\n#JISHOGI\n#WIN\n")
748     @next_player.write_safe("%KACHI\n#JISHOGI\n#LOSE\n")
749     @fh.printf("%%KACHI\n")
750     @fh.printf("'summary:kachi:%s win:%s lose\n", @current_player.name, @next_player.name)
751     @monitors.each do |monitor|
752       monitor.write_safe(sprintf("##[MONITOR][%s] %%KACHI\n", @id))
753     end
754   end
755
756   def toryo_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:toryo:%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 outori_win
769     @current_player.status = "connected"
770     @next_player.status = "connected"
771     @current_player.write_safe("#ILLEGAL_MOVE\n#WIN\n")
772     @next_player.write_safe("#ILLEGAL_MOVE\n#LOSE\n")
773     @fh.printf("'summary:outori:%s win:%s lose\n", @current_player.name, @next_player.name)
774     @monitors.each do |monitor|
775       monitor.write_safe(sprintf("##[MONITOR][%s] #ILLEGAL_MOVE\n", @id))
776     end
777   end
778
779   def start
780     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
781     @sente.write_safe(sprintf("START:%s\n", @id))
782     @gote.write_safe(sprintf("START:%s\n", @id))
783     @sente.mytime = @total_time
784     @gote.mytime = @total_time
785     @start_time = Time::new
786   end
787
788   def propose
789     begin
790       @fh = open(@logfile, "w")
791       @fh.sync = true
792
793       @fh.printf("V2\n")
794       @fh.printf("N+%s\n", @sente.name)
795       @fh.printf("N-%s\n", @gote.name)
796       @fh.printf("$EVENT:%s\n", @id)
797
798       @sente.write_safe(propose_message("+"))
799       @gote.write_safe(propose_message("-"))
800
801       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
802       @fh.print <<EOM
803 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
804 P2 * -HI *  *  *  *  * -KA *
805 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
806 P4 *  *  *  *  *  *  *  *  *
807 P5 *  *  *  *  *  *  *  *  *
808 P6 *  *  *  *  *  *  *  *  *
809 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
810 P8 * +KA *  *  *  *  * +HI *
811 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
812 +
813 EOM
814     end
815   end
816
817   def show()
818     str0 = <<EOM
819 BEGIN Game_Summary
820 Protocol_Version:1.0
821 Protocol_Mode:Server
822 Format:Shogi 1.0
823 Game_ID:#{@id}
824 Name+:#{@sente.name}
825 Name-:#{@gote.name}
826 Rematch_On_Draw:NO
827 To_Move:+
828 BEGIN Time
829 Time_Unit:1sec
830 Total_Time:#{@total_time}
831 Byoyomi:#{@byoyomi}
832 Remaining_Time+:#{@sente.mytime}
833 Remaining_Time-:#{@gote.mytime}
834 Last_Move:#{@last_move}
835 Current_Turn:#{@current_turn}
836 Least_Time_Per_Move:#{Least_Time_Per_Move}
837 END Time
838 BEGIN Position
839 Jishogi_Declaration:1.1
840 EOM
841
842     str1 = <<EOM
843 END Position
844 END Game_Summary
845 EOM
846
847     return str0 + @board.to_s + str1
848   end
849
850   def propose_message(sg_flag)
851     str = <<EOM
852 BEGIN Game_Summary
853 Protocol_Version:1.0
854 Protocol_Mode:Server
855 Format:Shogi 1.0
856 Game_ID:#{@id}
857 Name+:#{@sente.name}
858 Name-:#{@gote.name}
859 Your_Turn:#{sg_flag}
860 Rematch_On_Draw:NO
861 To_Move:+
862 BEGIN Time
863 Time_Unit:1sec
864 Total_Time:#{@total_time}
865 Byoyomi:#{@byoyomi}
866 Least_Time_Per_Move:#{Least_Time_Per_Move}
867 END Time
868 BEGIN Position
869 Jishogi_Declaration:1.1
870 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
871 P2 * -HI *  *  *  *  * -KA *
872 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
873 P4 *  *  *  *  *  *  *  *  *
874 P5 *  *  *  *  *  *  *  *  *
875 P6 *  *  *  *  *  *  *  *  *
876 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
877 P8 * +KA *  *  *  *  * +HI *
878 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
879 P+
880 P-
881 +
882 END Position
883 END Game_Summary
884 EOM
885     return str
886   end
887 end
888
889 def usage
890     print <<EOM
891 NAME
892         shogi-server - server for CSA server protocol
893
894 SYNOPSIS
895         shogi-server event_name port_number
896
897 DESCRIPTION
898         server for CSA server protocol
899
900 OPTIONS
901         --pid-file file
902                 specify filename for logging process ID
903
904 LICENSE
905         this file is distributed under GPL version2 and might be compiled by Exerb
906
907 SEE ALSO
908
909 RELEASE
910         #{Release}
911
912 REVISION
913         #{Revision}
914 EOM
915 end
916
917 def log_message(str)
918   printf("%s message: %s\n", Time::new.to_s, str)
919 end
920
921 def log_warning(str)
922   printf("%s warning: %s\n", Time::new.to_s, str)
923 end
924
925 def log_error(str)
926   printf("%s error: %s\n", Time::new.to_s, str)
927 end
928
929
930 def parse_command_line
931   options = Hash::new
932   parser = GetoptLong.new
933   parser.ordering = GetoptLong::REQUIRE_ORDER
934   parser.set_options(
935                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
936
937   parser.quiet = true
938   begin
939     parser.each_option do |name, arg|
940       name.sub!(/^--/, '')
941       options[name] = arg.dup
942     end
943   rescue
944     usage
945     raise parser.error_message
946   end
947   return options
948 end
949
950 def good_game_name?(str)
951   if ((str =~ /^(.+):\d+:\d+$/) &&
952       (good_identifier?($1)))
953     return true
954   else
955     return false
956   end
957 end
958
959 def good_identifier?(str)
960   if ((str =~ /\A[\w\d_@\-\.]+\z/) &&
961       (str.length < Max_Identifier_Length))
962     return true
963   else
964     return false
965   end
966 end
967
968 def good_login?(str)
969   tokens = str.split
970   if (((tokens.length == 3) || ((tokens.length == 4) && tokens[3] == "x1")) &&
971       (tokens[0] == "LOGIN") &&
972       (good_identifier?(tokens[1])))
973     return true
974   else
975     return false
976   end
977 end
978
979 def  write_pid_file(file)
980   open(file, "w") do |fh|
981     fh.print Process::pid, "\n"
982   end
983 end
984
985 def mutex_watchdog(mutex, sec)
986   while true
987     begin
988       timeout(sec) do
989         mutex.lock
990         mutex.unlock
991       end
992       sleep(sec)
993     rescue TimeoutError
994       log_error("mutex watchdog timeout")
995       exit(1)
996     end
997   end
998 end
999
1000 def main
1001   $mutex = Mutex::new
1002   Thread::start do
1003     mutex_watchdog($mutex, 10)
1004   end
1005
1006   $options = parse_command_line
1007   if (ARGV.length != 2)
1008     usage
1009     exit 2
1010   end
1011
1012   LEAGUE.event = ARGV.shift
1013   port = ARGV.shift
1014
1015   write_pid_file($options["pid-file"]) if ($options["pid-file"])
1016
1017
1018   Thread.abort_on_exception = true
1019
1020   server = TCPserver.open(port)
1021   log_message("server started")
1022
1023   while true
1024     Thread::start(server.accept) do |client|
1025       client.sync = true
1026       player = nil
1027       while (str = client.gets_timeout(Login_Time))
1028         begin
1029           $mutex.lock
1030           str =~ /([\r\n]*)$/
1031           eol = $1
1032           if (good_login?(str))
1033             player = Player::new(str, client)
1034             if (LEAGUE.players[player.name])
1035               if ((LEAGUE.players[player.name].password == player.password) &&
1036                   (LEAGUE.players[player.name].status != "game"))
1037                 log_message(sprintf("user %s login forcely", player.name))
1038                 LEAGUE.players[player.name].kill
1039               else
1040                 client.write_safe("LOGIN:incorrect" + eol)
1041                 client.write_safe(sprintf("username %s is already connected%s", player.name, eol)) if (str.split.length >= 4)
1042                 client.close
1043                 Thread::exit
1044               end
1045             end
1046             LEAGUE.add(player)
1047             break
1048           else
1049             client.write_safe("LOGIN:incorrect" + eol)
1050             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol) if (str.split.length >= 4)
1051           end
1052         ensure
1053           $mutex.unlock
1054         end
1055       end                       # login loop
1056       if (! player)
1057         client.close
1058         Thread::exit
1059       end
1060       log_message(sprintf("user %s login", player.name))
1061       player.run
1062       begin
1063         $mutex.lock
1064         if (player.game)
1065           player.game.kill(player)
1066         end
1067         player.finish
1068         LEAGUE.delete(player)
1069         log_message(sprintf("user %s logout", player.name))
1070       ensure
1071         $mutex.unlock
1072       end
1073     end
1074   end
1075 end
1076
1077 if ($0 == __FILE__)
1078   LEAGUE = League::new
1079   main
1080 end