OSDN Git Service

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