OSDN Git Service

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