OSDN Git Service

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