OSDN Git Service

6cb041d871cd2f536f86d26e9853e8fd00de5bc0
[shogi-server/shogi-server.git] / shogi-server
1 #! /usr/bin/env ruby
2 ## -*-Ruby-*- $RCSfile$ $Revision$ $Name$
3
4 ## Copyright (C) 2004 773@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 DEFAULT_TIMEOUT = 10            # for single socket operation
21 Total_Time = 1500
22 Least_Time_Per_Move = 1
23 Watchdog_Time = 30              # time for ping
24 Login_Time = 300                # time for LOGIN
25
26 Release = "$Name$".split[1].sub(/\A[^\d]*/, '').gsub(/_/, '.')
27 Release.concat("-") if (Release == "")
28 Revision = "$Revision$".gsub(/[^\.\d]/, '')
29
30 STDOUT.sync = true
31 STDERR.sync = true
32
33 require 'getoptlong'
34 require 'thread'
35 require 'timeout'
36 require 'socket'
37 require 'ping'
38
39 TCPSocket.do_not_reverse_lookup = true
40
41 class TCPSocket
42   def gets_timeout(t = DEFAULT_TIMEOUT)
43     begin
44       timeout(t) do
45         return self.gets
46       end
47     rescue TimeoutError
48       return nil
49     rescue
50       return nil
51     end
52   end
53   def gets_safe
54     begin
55       return self.gets
56     rescue
57       return nil
58     end
59   end
60   def write_safe(str)
61     begin
62       return self.write(str)
63     rescue
64       return nil
65     end
66   end
67 end
68
69
70 class League
71   def initialize
72     @hash = Hash::new
73   end
74   attr_accessor :hash
75
76   def add(player)
77     @hash[player.name] = player
78   end
79   def delete(player)
80     @hash.delete(player.name)
81   end
82   def duplicated?(player)
83     if (@hash[player.name])
84       return true
85     else
86       return false
87     end
88   end
89   def get_player(status, game_name, sente, searcher=nil)
90     @hash.each do |name, player|
91       if ((player.status == status) &&
92           (player.game_name == game_name) &&
93           ((player.sente == nil) || (player.sente == sente)) &&
94           ((searcher == nil) || (player != searcher)))
95         return player
96       end
97     end
98     return nil
99   end
100   def new_game(game_name, player0, player1)
101     game = Game::new(game_name, player0, player1)
102   end
103 end
104
105
106
107
108 class Player
109   def initialize(str, socket)
110     @name = nil
111     @password = nil
112     @socket = socket
113     @status = "connected"        # game_waiting -> agree_waiting -> start_waiting -> game
114
115     @protocol = nil             # CSA or x1
116     @eol = "\m"                 # favorite eol code
117     @game = nil
118     @game_name = ""
119     @mytime = Total_Time
120     @sente = nil
121     @watchdog_thread = nil
122
123     login(str)
124   end
125
126   attr_accessor :name, :password, :socket, :status
127   attr_accessor :protocol, :eol, :game, :mytime, :watchdog_thread, :game_name, :sente
128
129   def finish
130     log_message(sprintf("user %s finish", @name))    
131     Thread::kill(@watchdog_thread) if @watchdog_thread
132     @socket.close if (! @socket.closed?)
133   end
134
135   def watchdog(time)
136     while true
137       begin
138         Ping.pingecho(@socket.addr[3])
139       rescue
140       end
141       sleep(time)
142     end
143   end
144
145   def to_s
146     if ((status == "game_waiting") ||
147         (status == "agree_waiting") ||
148         (status == "game"))
149       if (@sente)
150         return sprintf("%s %s %s +", @name, @status, @game_name)
151       elsif (@sente == false)
152         return sprintf("%s %s %s -", @name, @status, @game_name)
153       elsif (@sente == nil)
154         return sprintf("%s %s %s +-", @name, @status, @game_name)
155       end
156     else
157       return sprintf("%s %s", @name, @status)
158     end
159   end
160
161   def write_help
162     @socket.write_safe('##[HELP] available commands "%%WHO", "%%CHAT str", "%%GAME game_name +", "%%GAME game_name -"')
163   end
164
165   def write_safe(str)
166     @socket.write_safe(str.gsub(/[\r\n]+/, @eol))
167   end
168
169   def login(str)
170     str =~ /([\r\n]*)$/
171     @eol = $1
172     str.chomp!
173     (login, @name, @password, ext) = str.split
174     if (ext)
175       @protocol = "x1"
176     else
177       @protocol = "CSA"
178     end
179     @watchdog_thread = Thread::start do
180       watchdog(Watchdog_Time)
181     end
182   end
183     
184   def run
185     if (@protocol != "CSA")
186       log_message(sprintf("user %s run in %s mode", @name, @protocol))
187       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
188     else
189       log_message(sprintf("user %s run in CSA mode", @name))
190       csa_1st_str = "%%GAME default +-"
191     end
192
193     
194     while (csa_1st_str || (str = @socket.gets_safe))
195       begin
196         $mutex.lock
197         if (csa_1st_str)
198           str = csa_1st_str
199           csa_1st_str = nil
200         end
201         str.chomp!
202         case str
203         when /^[\+\-%][^%]/
204           if (@status == "game")
205             s = @game.handle_one_move(str, self)
206             return if (s && @protocol == "CSA")
207           else
208             next
209           end
210         when /^AGREE/
211           if (@status == "agree_waiting")
212             @status = "start_waiting"
213             if ((@game.sente.status == "start_waiting") &&
214                 (@game.gote.status == "start_waiting"))
215               @game.start
216               @game.sente.status = "game"
217               @game.gote.status = "game"
218             end
219           else
220             write_safe("## you are in %s status. AGREE is valid in agree_waiting status\n", @status)
221             next
222           end
223         when /^%%HELP/
224           write_help
225         when /^%%GAME\s+(\S+)\s+([\+\-]+)/
226           if ((@status == "connected") || (@status == "game_waiting"))
227             @status = "game_waiting"
228           else
229             write_safe("## you are in %s status. GAME is valid in connected or game_waiting status\n", @status)
230             next
231           end
232           @status = "game_waiting"
233           @game_name = $1
234           sente_str = $2
235           if (sente_str == "+")
236             @sente = true
237             rival_sente = false
238           elsif (sente_str == "-")
239             @sente = false
240             rival_sente = true
241           else
242             @sente = nil
243             rival_sente = nil
244           end
245           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
246           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
247           if (rival)
248             if (@sente == nil)
249               if (rand(2) == 0)
250                 @sente = true
251                 rival_sente = false
252               else
253                 @sente = false
254                 rival_sente = true
255               end
256             elsif (rival_sente == nil)
257               if (@sente)
258                 rival_sente = false
259               else
260                 rival_sente = true
261               end
262             end
263             rival.sente = rival_sente
264             LEAGUE.new_game(@game_name, self, rival)
265             self.status = "agree_waiting"
266             rival.status = "agree_waiting"
267           end
268         when /^%%CHAT\s+(\S+)/
269           message = $1
270           LEAGUE.hash.each do |name, player|
271             if (player.protocol != "CSA")
272               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
273               player.status = "zombie" if (! s)
274             end
275           end
276         when /^%%WHO/
277           buf = Array::new
278           LEAGUE.hash.each do |name, player|
279             buf.push(sprintf("##[WHO] %s\n", player.to_s))
280           end
281           buf.push("##[WHO] +OK\n")
282           write_safe(buf.join)
283         when /^%%LOGOUT/
284           finish
285           return
286         else
287           write_safe(sprintf("## unknown command %s\n", str))
288         end
289       ensure
290         $mutex.unlock
291       end
292     end                         # enf of while
293   end
294 end
295
296 class Board
297 end
298
299 class Game
300   def initialize(game_name, player0, player1)
301     @game_name = game_name
302     if (player0.sente)
303       @sente = player0
304       @gote = player1
305     else
306       @sente = player1
307       @gote = player0
308     end
309     @current_player = @sente
310     @next_player = @gote
311
312     @sente.game = self
313     @gote.game = self
314     @sente.status = "agree_waiting"
315     @gote.status = "agree_waiting"
316     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
317     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
318
319     @logfile = @id + ".csa"
320     @board = Board::new
321     @start_time = nil
322     @fh = nil
323
324     propose
325   end
326   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
327
328   def finish
329     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
330     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
331     @fh.close
332     @sente.status = "connected"
333     @gote.status = "connected"
334     if (@current_player.protocol == "CSA")
335       @current_player.finish
336     end
337   end
338
339   def handle_one_move(str, player)
340     finish_flag = false
341     if (@current_player == player)
342       @end_time = Time::new
343       t = @end_time - @start_time
344       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
345       @sente.write_safe(sprintf("%s,T%d\n", str, t))
346       @gote.write_safe(sprintf("%s,T%d\n", str, t))
347       @fh.printf("%s\nT%d\n", str, t)
348       @current_player.mytime = @current_player.mytime - t
349       if (@current_player.mytime < 0)
350         timeout_end()
351         finish_flag = true
352       elsif (str =~ /%KACHI/)
353         kachi_end()
354         finish_flag = true
355       elsif (str =~ /%TORYO/)
356         toryo_end
357         finish_flag = true
358       end
359       (@current_player, @next_player) = [@next_player, @current_player]
360       @start_time = Time::new
361       finish if (finish_flag)
362       return finish_flag
363     end
364   end
365
366   def timeout_end
367     @current_player.status = "connected"
368     @next_player.status = "connected"
369     @current_player.write_safe("#TIME_UP\n#LOSE\n")
370     @next_player.write_safe("#TIME_UP\n#WIN\n")
371   end
372
373   def kachi_end
374     @current_player.status = "connected"
375     @next_player.status = "connected"
376     @current_player.write_safe("#JISHOGI\n#WIN\n")
377     @next_player.write_safe("#JISHOGI\n#LOSE\n")
378   end
379
380   def toryo_end
381     @current_player.status = "connected"
382     @next_player.status = "connected"
383     @current_player.write_safe("#RESIGN\n#LOSE\n")
384     @next_player.write_safe("#RESIGN\n#WIN\n")
385   end
386
387   def start
388     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
389     @sente.write_safe(sprintf("START:%s\n", @id))
390     @gote.write_safe(sprintf("START:%s\n", @id))
391     @start_time = Time::new
392   end
393
394   def propose
395     begin
396       @fh = open(@logfile, "w")
397       @fh.sync = true
398
399       @fh.printf("V2\n")
400       @fh.printf("N+%s\n", @sente.name)
401       @fh.printf("N-%s\n", @gote.name)
402       @fh.printf("$EVENT:%s\n", @id)
403
404       @sente.write_safe(propose_message("+"))
405       @gote.write_safe(propose_message("-"))
406
407       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
408       @fh.print <<EOM
409 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
410 P2 * -HI *  *  *  *  * -KA *
411 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
412 P4 *  *  *  *  *  *  *  *  *
413 P5 *  *  *  *  *  *  *  *  *
414 P6 *  *  *  *  *  *  *  *  *
415 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
416 P8 * +KA *  *  *  *  * +HI *
417 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
418 +
419 EOM
420     end
421   end
422
423   def propose_message(sg_flag)
424     str = <<EOM
425 Protocol_Mode:Server
426 Format:Shogi 1.0
427 Game_ID:#{@id}
428 Name+:#{@sente.name}
429 Name-:#{@gote.name}
430 Your_Turn:#{sg_flag}
431 Rematch_On_Draw:NO
432 To_Move:+
433 BEGIN Time
434 Time_Unit:1sec
435 Total_Time:#{Total_Time}
436 Least_Time_Per_Move:#{Least_Time_Per_Move}
437 END Time
438 BEGIN Position
439 Jishogi_Declaration:1.1
440 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
441 P2 * -HI *  *  *  *  * -KA *
442 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
443 P4 *  *  *  *  *  *  *  *  *
444 P5 *  *  *  *  *  *  *  *  *
445 P6 *  *  *  *  *  *  *  *  *
446 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
447 P8 * +KA *  *  *  *  * +HI *
448 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
449 P+
450 P-
451 +
452 END Position
453 END Game_Summary
454 EOM
455     return str
456   end
457 end
458
459 def usage
460     print <<EOM
461 NAME
462         shogi-server - server for CSA server protocol
463
464 SYNOPSIS
465         shogi-server event_name port_number
466
467 DESCRIPTION
468         server for CSA server protocol
469
470 OPTIONS
471         --pid-file file
472                 specify filename for logging process ID
473
474 LICENSE
475         this file is distributed under GPL version2 and might be compiled by Exerb
476
477 SEE ALSO
478
479 RELEASE
480         #{Release}
481
482 REVISION
483         #{Revision}
484 EOM
485 end
486
487 def log_message(str)
488   printf("%s message: %s\n", Time::new.to_s, str)
489 end
490
491 def log_warning(str)
492   printf("%s message: %s\n", Time::new.to_s, str)
493 end
494
495 def log_error(str)
496   printf("%s error: %s\n", Time::new.to_s, str)
497 end
498
499
500 def parse_command_line
501   options = Hash::new
502   parser = GetoptLong.new
503   parser.ordering = GetoptLong::REQUIRE_ORDER
504   parser.set_options(
505                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
506
507   parser.quiet = true
508   begin
509     parser.each_option do |name, arg|
510       name.sub!(/^--/, '')
511       options[name] = arg.dup
512     end
513   rescue
514     usage
515     raise parser.error_message
516   end
517   return options
518 end
519
520 LEAGUE = League::new
521
522 def good_login?(str)
523   return false if (str !~ /^LOGIN /)
524   tokens = str.split
525   if ((tokens.length == 3) || 
526       ((tokens.length == 4) && tokens[3] == "x1"))
527     ## ok
528   else
529     return false
530   end
531   return true
532 end
533
534 def  write_pid_file(file)
535   open(file, "w") do |fh|
536     fh.print Process::pid, "\n"
537   end
538 end
539
540 def main
541   $mutex = Mutex::new
542   $options = parse_command_line
543   if (ARGV.length != 2)
544     usage
545     exit 2
546   end
547   event = ARGV.shift
548   port = ARGV.shift
549
550   write_pid_file($options["pid-file"]) if ($options["pid-file"])
551
552
553   Thread.abort_on_exception = true
554
555   server = TCPserver.open(port)
556   log_message("server started")
557
558   while true
559     Thread::start(server.accept) do |client|
560       client.sync = true
561       player = nil
562       while (str = client.gets_timeout(Login_Time))
563         begin
564           $mutex.lock
565           Thread::kill(Thread::current) if (! str) # disconnected
566           str =~ /([\r\n]*)$/
567           eol = $1
568           if (good_login?(str))
569             player = Player::new(str, client)
570             if (LEAGUE.duplicated?(player))
571               client.write_safe(sprintf("username %s is already connected%s", player.name, eol))
572               client.close
573               Thread::kill(Thread::current)
574             end
575             LEAGUE.add(player)
576             break
577           else
578             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol)
579             client.close
580             Thread::kill(Thread::current)
581           end
582         ensure
583           $mutex.unlock
584         end
585       end                       # login loop
586       log_message(sprintf("user %s login", player.name))
587       player.run
588       LEAGUE.delete(player)
589       log_message(sprintf("user %s logout", player.name))
590     end
591   end
592 end
593
594 if ($0 == __FILE__)
595   main
596 end