OSDN Git Service

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