OSDN Git Service

c82597de0680b533aba3f7d456ea84344db209ec
[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        # set in start method also
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     write_safe(sprintf("LOGIN:%s OK\n", @name))
186     if (@protocol != "CSA")
187       log_message(sprintf("user %s run in %s mode", @name, @protocol))
188       write_safe(sprintf("##[LOGIN] +OK %s\n", @protocol))
189     else
190       log_message(sprintf("user %s run in CSA mode", @name))
191       csa_1st_str = "%%GAME default +-"
192     end
193
194     
195     while (csa_1st_str || (str = @socket.gets_safe))
196       begin
197         $mutex.lock
198         if (csa_1st_str)
199           str = csa_1st_str
200           csa_1st_str = nil
201         end
202         str.chomp!
203         case str
204         when /^[\+\-%][^%]/
205           if (@status == "game")
206             s = @game.handle_one_move(str, self)
207             return if (s && @protocol == "CSA")
208           else
209             next
210           end
211         when /^AGREE/
212           if (@status == "agree_waiting")
213             @status = "start_waiting"
214             if ((@game.sente.status == "start_waiting") &&
215                 (@game.gote.status == "start_waiting"))
216               @game.start
217               @game.sente.status = "game"
218               @game.gote.status = "game"
219             end
220           else
221             write_safe("##[ERROR] you are in %s status. AGREE is valid in agree_waiting status\n", @status)
222             next
223           end
224         when /^%%HELP/
225           write_help
226         when /^%%GAME\s+(\S+)\s+([\+\-]+)/
227           if ((@status == "connected") || (@status == "game_waiting"))
228             @status = "game_waiting"
229           else
230             write_safe(sprintf("##[ERROR] you are in %s status. GAME is valid in connected or game_waiting status\n", @status))
231             next
232           end
233           @status = "game_waiting"
234           @game_name = $1
235           sente_str = $2
236           if (sente_str == "+")
237             @sente = true
238             rival_sente = false
239           elsif (sente_str == "-")
240             @sente = false
241             rival_sente = true
242           else
243             @sente = nil
244             rival_sente = nil
245           end
246           rival = LEAGUE.get_player("game_waiting", @game_name, rival_sente, self)
247           rival = LEAGUE.get_player("game_waiting", @game_name, nil, self) if (! rival)
248           if (rival)
249             if (@sente == nil)
250               if (rand(2) == 0)
251                 @sente = true
252                 rival_sente = false
253               else
254                 @sente = false
255                 rival_sente = true
256               end
257             elsif (rival_sente == nil)
258               if (@sente)
259                 rival_sente = false
260               else
261                 rival_sente = true
262               end
263             end
264             rival.sente = rival_sente
265             LEAGUE.new_game(@game_name, self, rival)
266             self.status = "agree_waiting"
267             rival.status = "agree_waiting"
268           end
269         when /^%%CHAT\s+(\S+)/
270           message = $1
271           LEAGUE.hash.each do |name, player|
272             if (player.protocol != "CSA")
273               s = player.write_safe(sprintf("##[CHAT][%s] %s\n", @name, message)) 
274               player.status = "zombie" if (! s)
275             end
276           end
277         when /^%%WHO/
278           buf = Array::new
279           LEAGUE.hash.each do |name, player|
280             buf.push(sprintf("##[WHO] %s\n", player.to_s))
281           end
282           buf.push("##[WHO] +OK\n")
283           write_safe(buf.join)
284         when /^LOGOUT/
285           write_safe("LOGOUT:completed\n")
286           finish
287           return
288         else
289           write_safe(sprintf("##[ERROR] unknown command %s\n", str))
290         end
291       ensure
292         $mutex.unlock
293       end
294     end                         # enf of while
295   end
296 end
297
298 class Board
299 end
300
301 class Game
302   def initialize(game_name, player0, player1)
303     @game_name = game_name
304     if (player0.sente)
305       @sente = player0
306       @gote = player1
307     else
308       @sente = player1
309       @gote = player0
310     end
311     @current_player = @sente
312     @next_player = @gote
313
314     @sente.game = self
315     @gote.game = self
316     @sente.status = "agree_waiting"
317     @gote.status = "agree_waiting"
318     @id = sprintf("%s-%s-%s-%s", @game_name, @sente.name, @gote.name, Time::new.strftime("%Y%m%d%H%M%S"))
319     log_message(sprintf("game created %s %s %s", game_name, sente.name, gote.name))
320
321     @logfile = @id + ".csa"
322     @board = Board::new
323     @start_time = nil
324     @fh = nil
325
326     propose
327   end
328   attr_accessor :game_name, :sente, :gote, :id, :board, :current_player, :next_player, :fh
329
330   def finish
331     log_message(sprintf("game finished %s %s %s", game_name, sente.name, gote.name))
332     @fh.printf("'$END_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))    
333     @fh.close
334     @sente.status = "connected"
335     @gote.status = "connected"
336     if (@current_player.protocol == "CSA")
337       @current_player.finish
338     end
339   end
340
341   def handle_one_move(str, player)
342     finish_flag = false
343     if (@current_player == player)
344       @end_time = Time::new
345       t = @end_time - @start_time
346       t = Least_Time_Per_Move if (t < Least_Time_Per_Move)
347       @sente.write_safe(sprintf("%s,T%d\n", str, t))
348       @gote.write_safe(sprintf("%s,T%d\n", str, t))
349       @fh.printf("%s\nT%d\n", str, t)
350       @current_player.mytime = @current_player.mytime - t
351       if (@current_player.mytime < 0)
352         timeout_end()
353         finish_flag = true
354       elsif (str =~ /%KACHI/)
355         kachi_end()
356         finish_flag = true
357       elsif (str =~ /%TORYO/)
358         toryo_end
359         finish_flag = true
360       end
361       (@current_player, @next_player) = [@next_player, @current_player]
362       @start_time = Time::new
363       finish if (finish_flag)
364       return finish_flag
365     end
366   end
367
368   def timeout_end
369     @current_player.status = "connected"
370     @next_player.status = "connected"
371     @current_player.write_safe("#TIME_UP\n#LOSE\n")
372     @next_player.write_safe("#TIME_UP\n#WIN\n")
373   end
374
375   def kachi_end
376     @current_player.status = "connected"
377     @next_player.status = "connected"
378     @current_player.write_safe("#JISHOGI\n#WIN\n")
379     @next_player.write_safe("#JISHOGI\n#LOSE\n")
380   end
381
382   def toryo_end
383     @current_player.status = "connected"
384     @next_player.status = "connected"
385     @current_player.write_safe("#RESIGN\n#LOSE\n")
386     @next_player.write_safe("#RESIGN\n#WIN\n")
387   end
388
389   def start
390     log_message(sprintf("game started %s %s %s", game_name, sente.name, gote.name))
391     @sente.write_safe(sprintf("START:%s\n", @id))
392     @gote.write_safe(sprintf("START:%s\n", @id))
393     @mytime = Total_Time    
394     @start_time = Time::new
395   end
396
397   def propose
398     begin
399       @fh = open(@logfile, "w")
400       @fh.sync = true
401
402       @fh.printf("V2\n")
403       @fh.printf("N+%s\n", @sente.name)
404       @fh.printf("N-%s\n", @gote.name)
405       @fh.printf("$EVENT:%s\n", @id)
406
407       @sente.write_safe(propose_message("+"))
408       @gote.write_safe(propose_message("-"))
409
410       @fh.printf("$START_TIME:%s\n", Time::new.strftime("%Y/%m/%d %H:%M:%S"))
411       @fh.print <<EOM
412 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
413 P2 * -HI *  *  *  *  * -KA *
414 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
415 P4 *  *  *  *  *  *  *  *  *
416 P5 *  *  *  *  *  *  *  *  *
417 P6 *  *  *  *  *  *  *  *  *
418 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
419 P8 * +KA *  *  *  *  * +HI *
420 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
421 +
422 EOM
423     end
424   end
425
426   def propose_message(sg_flag)
427     str = <<EOM
428 Protocol_Mode:Server
429 Format:Shogi 1.0
430 Game_ID:#{@id}
431 Name+:#{@sente.name}
432 Name-:#{@gote.name}
433 Your_Turn:#{sg_flag}
434 Rematch_On_Draw:NO
435 To_Move:+
436 BEGIN Time
437 Time_Unit:1sec
438 Total_Time:#{Total_Time}
439 Least_Time_Per_Move:#{Least_Time_Per_Move}
440 END Time
441 BEGIN Position
442 Jishogi_Declaration:1.1
443 P1-KY-KE-GI-KI-OU-KI-GI-KE-KY
444 P2 * -HI *  *  *  *  * -KA *
445 P3-FU-FU-FU-FU-FU-FU-FU-FU-FU
446 P4 *  *  *  *  *  *  *  *  *
447 P5 *  *  *  *  *  *  *  *  *
448 P6 *  *  *  *  *  *  *  *  *
449 P7+FU+FU+FU+FU+FU+FU+FU+FU+FU
450 P8 * +KA *  *  *  *  * +HI *
451 P9+KY+KE+GI+KI+OU+KI+GI+KE+KY
452 P+
453 P-
454 +
455 END Position
456 END Game_Summary
457 EOM
458     return str
459   end
460 end
461
462 def usage
463     print <<EOM
464 NAME
465         shogi-server - server for CSA server protocol
466
467 SYNOPSIS
468         shogi-server event_name port_number
469
470 DESCRIPTION
471         server for CSA server protocol
472
473 OPTIONS
474         --pid-file file
475                 specify filename for logging process ID
476
477 LICENSE
478         this file is distributed under GPL version2 and might be compiled by Exerb
479
480 SEE ALSO
481
482 RELEASE
483         #{Release}
484
485 REVISION
486         #{Revision}
487 EOM
488 end
489
490 def log_message(str)
491   printf("%s message: %s\n", Time::new.to_s, str)
492 end
493
494 def log_warning(str)
495   printf("%s message: %s\n", Time::new.to_s, str)
496 end
497
498 def log_error(str)
499   printf("%s error: %s\n", Time::new.to_s, str)
500 end
501
502
503 def parse_command_line
504   options = Hash::new
505   parser = GetoptLong.new
506   parser.ordering = GetoptLong::REQUIRE_ORDER
507   parser.set_options(
508                      ["--pid-file", GetoptLong::REQUIRED_ARGUMENT])
509
510   parser.quiet = true
511   begin
512     parser.each_option do |name, arg|
513       name.sub!(/^--/, '')
514       options[name] = arg.dup
515     end
516   rescue
517     usage
518     raise parser.error_message
519   end
520   return options
521 end
522
523 LEAGUE = League::new
524
525 def good_login?(str)
526   return false if (str !~ /^LOGIN /)
527   tokens = str.split
528   if ((tokens.length == 3) || 
529       ((tokens.length == 4) && tokens[3] == "x1"))
530     ## ok
531   else
532     return false
533   end
534   return true
535 end
536
537 def  write_pid_file(file)
538   open(file, "w") do |fh|
539     fh.print Process::pid, "\n"
540   end
541 end
542
543 def main
544   $mutex = Mutex::new
545   $options = parse_command_line
546   if (ARGV.length != 2)
547     usage
548     exit 2
549   end
550   event = ARGV.shift
551   port = ARGV.shift
552
553   write_pid_file($options["pid-file"]) if ($options["pid-file"])
554
555
556   Thread.abort_on_exception = true
557
558   server = TCPserver.open(port)
559   log_message("server started")
560
561   while true
562     Thread::start(server.accept) do |client|
563       client.sync = true
564       player = nil
565       while (str = client.gets_timeout(Login_Time))
566         begin
567           $mutex.lock
568           Thread::kill(Thread::current) if (! str) # disconnected
569           str =~ /([\r\n]*)$/
570           eol = $1
571           if (good_login?(str))
572             player = Player::new(str, client)
573             if (LEAGUE.duplicated?(player))
574               client.write_safe("LOGIN:incorrect" + eol)
575               client.write_safe(sprintf("username %s is already connected%s", player.name, eol))
576               client.close
577               Thread::kill(Thread::current)
578             end
579             LEAGUE.add(player)
580             break
581           else
582             client.write_safe("LOGIN:incorrect" + eol)
583             client.write_safe("type 'LOGIN name password' or 'LOGIN name password x1'" + eol)
584             client.close
585             Thread::kill(Thread::current)
586           end
587         ensure
588           $mutex.unlock
589         end
590       end                       # login loop
591       log_message(sprintf("user %s login", player.name))
592       player.run
593       LEAGUE.delete(player)
594       log_message(sprintf("user %s logout", player.name))
595     end
596   end
597 end
598
599 if ($0 == __FILE__)
600   main
601 end