OSDN Git Service

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